Simple Spectral Analysis Functions
kravlor
#pragma rtGlobals=1 // Use modern global access method.
Function CrossPower(srcX, srcY, bw_res)
WAVE srcX, srcY
Variable bw_res // Desired spectral resolution, Hz
Variable retval
retval = CrossPowerDensity(srcX, srcY, bw_res)
if(retval < 0)
return -1 // Error!
endif
WAVE/C G_XY
Duplicate/C/O G_XY P_XY
Redimension/R P_XY
WAVE pxy = P_XY
// Cross-power is modulus of G_XY
pxy = cabs(G_XY)
// Set units correctly; should be (srcUnits)^2 / Hz
SetScale d, 0, 0, "(" + WaveUnits(srcX, 1) + ")^2/Hz", pxy
KillWaves/Z G_XY
End
Function Coherence(srcX, srcY, bw_res, [noBiasError])
WAVE srcX, srcY
Variable bw_res // Desired spectral resolution, Hz
Variable noBiasError // Flag; set to 1 to skip computation of bias error
if(ParamIsDefault(noBiasError))
noBiasError = 0
endif
// Compute Cross- and Autospectral density estimates from the source waves
Variable retval
retval =CrossPowerDensity(srcX, srcX, bw_res)
if(retval < 0)
return -1 // Error!
endif
WAVE/C G_XY
Duplicate/O/C G_XY G_XX
CrossPowerDensity(srcY, srcY, bw_res)
WAVE/C G_XY
Duplicate/O/C G_XY G_YY
CrossPowerDensity(srcX, srcY, bw_res)
WAVE/C G_XY
Duplicate/O/C G_XY gamma_XY
gamma_XY *= conj(G_XY) // |G_xy|^2 == G_xy * conj(G_xy)
gamma_XY /= (G_XX * G_YY)
Redimension/R gamma_XY
// coherence is dimensionless
SetScale d, 0, 0, "", gamma_XY
Duplicate/O gamma_xy unc_gamma_xy, bias_gamma_xy
NVAR Nchunk
unc_gamma_xy = sqrt(2)*(1-gamma_xy) / (sqrt(gamma_xy) * sqrt(Nchunk)) // 1-std deviation, Bendat 11.47, p 275 (random error)
if(!noBiasError)
bias_gamma_xy = coherenceBiasError(gamma_xy, Nchunk)
else
KillWaves/Z bias_gamma_xy
endif
//Xcoher = sqrt( magsqr(G_XY) / (G_XX * G_YY) )
// Compute coherent output power spectrum, Bendat 3.71; g_y:x = gamma_xy(f) * g_yy(f)
Duplicate/O G_YY, cohP_YX
Redimension/R cohP_YX
cohP_YX = gamma_XY * cabs(G_YY)
// Set units correctly; should be (srcUnits)^2 / Hz (same as cross-power, since gamma_xy is dimensionless)
SetScale d, 0, 0, "(" + WaveUnits(srcY, 1) + ")^2/Hz", cohP_YX
KillWaves/Z G_XX, G_YY, G_XY
End
// coherenceBiasError: Given an estimate of coherence, and the number of averages used in the
// FFT computation of the Gxy values to derive it, returns the 1-sigma bias error.
// Equation IIe from Carter1973; IIa is available in commented-out portion due to compute inefficiency.
Function coherenceBiasError(gamSqrEst, Nd)
Variable gamSqrEst // coherence estimated from Nd spectral averages
Variable Nd // number of distinct FFT averages used to compute gamSqrEstimate
// Equation IIa; exact, but sometimes leads to infinite compute time in hyperG2F1 function.
// Return (1/Nd) + (Nd - 1)/(Nd + 1) * gamSqrEst * hyperG2F1(1,1,Nd+2,gamSqrEst) - gamSqrEst
Return (1/Nd)*(1-gamSqrEst)^2 // Eq. IIe, approximation for large Nd
End
Function CrossPhase(srcX, srcY, bw_res)
WAVE srcX, srcY
Variable bw_res // Desired spectral resolution, Hz
Variable retval
retval = CrossPowerDensity(srcX, srcY, bw_res)
if(retval < 0)
return -1 // Error!
endif
WAVE/C G_XY
Duplicate/O/C G_XY theta_XY
Redimension/R theta_XY
WAVE xphs = theta_XY
xphs = atan2(imag(G_XY) , real(G_XY)) * 180/Pi
// Units...
SetScale d, 0, 0, "Degrees", xphs
// Uncertainty estimate: need gamma_xy!
coherence(srcX, srcY, bw_res, noBiasError=1)
WAVE gamma_xy
Duplicate/O xphs unc_theta_XY
NVAR Nchunk
// 1-std deviation formula; Bendat 11.62.
unc_theta_XY = sqrt(1-gamma_xy)/(sqrt(gamma_xy)*sqrt(2*Nchunk)) * 180/Pi
KillWaves/Z G_XY
End
// CrossPowerDensity: Computes the estimated cross-power spectral density G_XY given two source waves
// of identical length, units, and scaling. Creates (and overwrites if pre-existing) destination wave G_XY,
// which is the result of computing Bendat eq. 3.90, p. 72.
// Note that autopower spectral density estimates can be also computed with this function, letting the
// srcX and srcY waves be identical.
Function CrossPowerDensity(srcX, srcY, bw_res)
WAVE srcX, srcY
Variable bw_res // Desired spectral resolution, Hz
if(numpnts(srcX) != numpnts(srcY) || !((deltax(srcX) - deltax(srcY)) < 1e-8))
// Waves are of incompatible scale
print "CrossPowerDensity is not yet equipped to handle differently-scaled waves. Aborting..."
print numpnts(srcX), numpnts(srcY), numpnts(srcX)-numpnts(srcY)
print deltax(srcX), deltax(srcY), deltax(srcX)-deltax(srcY)
return -1
endif
fCrossPowerSpectralDensity(srcX, srcY, bw_res)
WAVE/C G_XY
// Correct Units on cross-spectral density result
SetScale d, 0, 0, "(" + WaveUnits(srcX, 1) + ")^2/Hz", G_XY
End
// fCrossPowerSpectralDensity: Computes the complex cross power spectral density Gxy between waves
// w, w2 given a bandwidth resolution request bw_res.
// Expanded from the Wavemetrics fPowerSpectralDensity function, which computes a properly normalized
// (i.e. time-average power conserving) spectral density of a single wave (autopower spectral density).
// fCrossPowerSpectralDensity may be used to compute autopower spectra as well by specifying w = w2.
Function/S fCrossPowerSpectralDensity(w, w2, bw_res)
Wave w, w2
Variable bw_res // Desired bandwidth, Hz
// Variable npsd // SegLen - the number of input points in each segment
// String windowName // one of "Square;Hann;Parzen;Welch;Hamming;BlackmanHarris3;KaiserBessel"
Variable removeDC = 1
Variable npsd = getNpsd(w, bw_res)
String destw="G_XY"
String srctmp=NameOfWave(w)+"_tmp"
String srctmp2 =NameOfWave(w)+"_tm2"
String winw=NameOfWave(w)+"_psdWin"
Make/O/C/N=(npsd/2+1) $destw= 0
WAVE/C psd= $destw
Make/O/N=(npsd) $srctmp,$winw, $srctmp2
WAVE tmp= $srctmp
Wave tmp2 = $srctmp2
WAVE win= $winw
win= 1
Variable winNorm
Hanning win
// winNorm=0.375 // theoretical avg squared value
WaveStats/Q win
winNorm= V_rms*V_rms // actual value is more accurate than a theoretical value.
Variable dc= 0
Variable dc2 = 0
if( removeDC ) // boolean
WaveStats/Q w
dc= V_Avg
WaveStats/Q w2
dc2 = V_Avg
endif
Variable psdFirst= 0
Variable psdOffset= npsd/2
Variable nsrc= numpnts(w)
Variable nsegs
for( nsegs= 0; psdFirst+npsd <= nsrc; nsegs += 1, psdFirst += psdOffset)
Duplicate/O/R=[psdFirst,psdFirst+npsd-1] w, tmp
Duplicate/O/R=[psdFirst,psdFirst+npsd-1] w2, tmp2
if(mod(numpnts(tmp), 2)) // odd; add one point of zero padding to end of element
InsertPoints (numpnts(tmp)-1), 1, tmp, tmp2
endif
tmp = (tmp-dc) * win
tmp2 = (tmp2-dc) * win
FFT/DEST=ctmp tmp // result is a one-sided spectrum of complex values
FFT/DEST=ctmp2 tmp2
WAVE/C ctmp, ctmp2
psd += conj(ctmp)*ctmp2 // summed Fourier power of one-sided spectrum
// (we're missing all negative frequency powers except for the Nyquist frequency)
endfor
Variable/G Nchunk = floor(nsegs + 1) / 2
// Print "Used",Nchunk, "unique bins"
CopyScales/P ctmp, psd
// normalize the sum of PSDs
Variable deltaF= deltax(psd) // deltaF is the frequency bin width
Variable norm= 2 / (npsd * npsd * nsegs * winNorm * deltaF)
psd *= norm
// Explanation of norm values:
// * 2 total power = magnitude^2(-f) + magnitude^2(f), and we've only accumulated magnitude^2(f)
// / (npsd * npsd * nsegs) converts to average power
// / winNorm compensates for power lost by windowing the time data.
// / deltaF converts power to power density (per Hertz)
psd[0] /= 2 // we're not missing 1/2 the power of DC bin from the two-sided FFT, restore original value
psd[npsd/2] /= 2 // there aren't two Nyquist bins, either.
// Parseval's theorem (power in time-domain = power in frequency domain)
// is satisfied if you compare:
// time-domain average power ("mean squared amplitude" in Numerical Recipies) = 1/N * sum(t=0...N-1) w[t]^2
// frequency-domain average power= deltaf * sum(f=0...npsd/2) destw[f]^2
KillWaves/Z tmp, tmp2, win, ctmp, ctmp2
return NameOfWave(psd) // in the current data folder
End
// getNpsd: Given a source wave, computes the number of points to use for the
// 'npsd' parameter in fCrossPowerSpectralDensity to obtain an effective bandwidth
// of bw_res.
// Also, marginally improved to improve resilience against unattainable bandwidth requests.
Function getNpsd(src, bw_res)
WAVE src
Variable bw_res
NVAR/Z quiet
Variable nsrc = numpnts(src)
Variable nBins = (rightX(src) - leftX(src))*bw_res
Variable nPsd = floor(4*(nsrc/(2*nBins)))
Variable roundBins = floor(nBins)
if(roundBins < 1 || nPsd > nsrc)
nPsd = nsrc
if(NVAR_Exists(quiet))
if(quiet == 1)
return nPsd
endif
endif
Print "getNpsd: Requested resolution of", bw_res,"is not possible; using one bin..."
endif
return nPsd
End
Function CrossPower(srcX, srcY, bw_res)
WAVE srcX, srcY
Variable bw_res // Desired spectral resolution, Hz
Variable retval
retval = CrossPowerDensity(srcX, srcY, bw_res)
if(retval < 0)
return -1 // Error!
endif
WAVE/C G_XY
Duplicate/C/O G_XY P_XY
Redimension/R P_XY
WAVE pxy = P_XY
// Cross-power is modulus of G_XY
pxy = cabs(G_XY)
// Set units correctly; should be (srcUnits)^2 / Hz
SetScale d, 0, 0, "(" + WaveUnits(srcX, 1) + ")^2/Hz", pxy
KillWaves/Z G_XY
End
Function Coherence(srcX, srcY, bw_res, [noBiasError])
WAVE srcX, srcY
Variable bw_res // Desired spectral resolution, Hz
Variable noBiasError // Flag; set to 1 to skip computation of bias error
if(ParamIsDefault(noBiasError))
noBiasError = 0
endif
// Compute Cross- and Autospectral density estimates from the source waves
Variable retval
retval =CrossPowerDensity(srcX, srcX, bw_res)
if(retval < 0)
return -1 // Error!
endif
WAVE/C G_XY
Duplicate/O/C G_XY G_XX
CrossPowerDensity(srcY, srcY, bw_res)
WAVE/C G_XY
Duplicate/O/C G_XY G_YY
CrossPowerDensity(srcX, srcY, bw_res)
WAVE/C G_XY
Duplicate/O/C G_XY gamma_XY
gamma_XY *= conj(G_XY) // |G_xy|^2 == G_xy * conj(G_xy)
gamma_XY /= (G_XX * G_YY)
Redimension/R gamma_XY
// coherence is dimensionless
SetScale d, 0, 0, "", gamma_XY
Duplicate/O gamma_xy unc_gamma_xy, bias_gamma_xy
NVAR Nchunk
unc_gamma_xy = sqrt(2)*(1-gamma_xy) / (sqrt(gamma_xy) * sqrt(Nchunk)) // 1-std deviation, Bendat 11.47, p 275 (random error)
if(!noBiasError)
bias_gamma_xy = coherenceBiasError(gamma_xy, Nchunk)
else
KillWaves/Z bias_gamma_xy
endif
//Xcoher = sqrt( magsqr(G_XY) / (G_XX * G_YY) )
// Compute coherent output power spectrum, Bendat 3.71; g_y:x = gamma_xy(f) * g_yy(f)
Duplicate/O G_YY, cohP_YX
Redimension/R cohP_YX
cohP_YX = gamma_XY * cabs(G_YY)
// Set units correctly; should be (srcUnits)^2 / Hz (same as cross-power, since gamma_xy is dimensionless)
SetScale d, 0, 0, "(" + WaveUnits(srcY, 1) + ")^2/Hz", cohP_YX
KillWaves/Z G_XX, G_YY, G_XY
End
// coherenceBiasError: Given an estimate of coherence, and the number of averages used in the
// FFT computation of the Gxy values to derive it, returns the 1-sigma bias error.
// Equation IIe from Carter1973; IIa is available in commented-out portion due to compute inefficiency.
Function coherenceBiasError(gamSqrEst, Nd)
Variable gamSqrEst // coherence estimated from Nd spectral averages
Variable Nd // number of distinct FFT averages used to compute gamSqrEstimate
// Equation IIa; exact, but sometimes leads to infinite compute time in hyperG2F1 function.
// Return (1/Nd) + (Nd - 1)/(Nd + 1) * gamSqrEst * hyperG2F1(1,1,Nd+2,gamSqrEst) - gamSqrEst
Return (1/Nd)*(1-gamSqrEst)^2 // Eq. IIe, approximation for large Nd
End
Function CrossPhase(srcX, srcY, bw_res)
WAVE srcX, srcY
Variable bw_res // Desired spectral resolution, Hz
Variable retval
retval = CrossPowerDensity(srcX, srcY, bw_res)
if(retval < 0)
return -1 // Error!
endif
WAVE/C G_XY
Duplicate/O/C G_XY theta_XY
Redimension/R theta_XY
WAVE xphs = theta_XY
xphs = atan2(imag(G_XY) , real(G_XY)) * 180/Pi
// Units...
SetScale d, 0, 0, "Degrees", xphs
// Uncertainty estimate: need gamma_xy!
coherence(srcX, srcY, bw_res, noBiasError=1)
WAVE gamma_xy
Duplicate/O xphs unc_theta_XY
NVAR Nchunk
// 1-std deviation formula; Bendat 11.62.
unc_theta_XY = sqrt(1-gamma_xy)/(sqrt(gamma_xy)*sqrt(2*Nchunk)) * 180/Pi
KillWaves/Z G_XY
End
// CrossPowerDensity: Computes the estimated cross-power spectral density G_XY given two source waves
// of identical length, units, and scaling. Creates (and overwrites if pre-existing) destination wave G_XY,
// which is the result of computing Bendat eq. 3.90, p. 72.
// Note that autopower spectral density estimates can be also computed with this function, letting the
// srcX and srcY waves be identical.
Function CrossPowerDensity(srcX, srcY, bw_res)
WAVE srcX, srcY
Variable bw_res // Desired spectral resolution, Hz
if(numpnts(srcX) != numpnts(srcY) || !((deltax(srcX) - deltax(srcY)) < 1e-8))
// Waves are of incompatible scale
print "CrossPowerDensity is not yet equipped to handle differently-scaled waves. Aborting..."
print numpnts(srcX), numpnts(srcY), numpnts(srcX)-numpnts(srcY)
print deltax(srcX), deltax(srcY), deltax(srcX)-deltax(srcY)
return -1
endif
fCrossPowerSpectralDensity(srcX, srcY, bw_res)
WAVE/C G_XY
// Correct Units on cross-spectral density result
SetScale d, 0, 0, "(" + WaveUnits(srcX, 1) + ")^2/Hz", G_XY
End
// fCrossPowerSpectralDensity: Computes the complex cross power spectral density Gxy between waves
// w, w2 given a bandwidth resolution request bw_res.
// Expanded from the Wavemetrics fPowerSpectralDensity function, which computes a properly normalized
// (i.e. time-average power conserving) spectral density of a single wave (autopower spectral density).
// fCrossPowerSpectralDensity may be used to compute autopower spectra as well by specifying w = w2.
Function/S fCrossPowerSpectralDensity(w, w2, bw_res)
Wave w, w2
Variable bw_res // Desired bandwidth, Hz
// Variable npsd // SegLen - the number of input points in each segment
// String windowName // one of "Square;Hann;Parzen;Welch;Hamming;BlackmanHarris3;KaiserBessel"
Variable removeDC = 1
Variable npsd = getNpsd(w, bw_res)
String destw="G_XY"
String srctmp=NameOfWave(w)+"_tmp"
String srctmp2 =NameOfWave(w)+"_tm2"
String winw=NameOfWave(w)+"_psdWin"
Make/O/C/N=(npsd/2+1) $destw= 0
WAVE/C psd= $destw
Make/O/N=(npsd) $srctmp,$winw, $srctmp2
WAVE tmp= $srctmp
Wave tmp2 = $srctmp2
WAVE win= $winw
win= 1
Variable winNorm
Hanning win
// winNorm=0.375 // theoretical avg squared value
WaveStats/Q win
winNorm= V_rms*V_rms // actual value is more accurate than a theoretical value.
Variable dc= 0
Variable dc2 = 0
if( removeDC ) // boolean
WaveStats/Q w
dc= V_Avg
WaveStats/Q w2
dc2 = V_Avg
endif
Variable psdFirst= 0
Variable psdOffset= npsd/2
Variable nsrc= numpnts(w)
Variable nsegs
for( nsegs= 0; psdFirst+npsd <= nsrc; nsegs += 1, psdFirst += psdOffset)
Duplicate/O/R=[psdFirst,psdFirst+npsd-1] w, tmp
Duplicate/O/R=[psdFirst,psdFirst+npsd-1] w2, tmp2
if(mod(numpnts(tmp), 2)) // odd; add one point of zero padding to end of element
InsertPoints (numpnts(tmp)-1), 1, tmp, tmp2
endif
tmp = (tmp-dc) * win
tmp2 = (tmp2-dc) * win
FFT/DEST=ctmp tmp // result is a one-sided spectrum of complex values
FFT/DEST=ctmp2 tmp2
WAVE/C ctmp, ctmp2
psd += conj(ctmp)*ctmp2 // summed Fourier power of one-sided spectrum
// (we're missing all negative frequency powers except for the Nyquist frequency)
endfor
Variable/G Nchunk = floor(nsegs + 1) / 2
// Print "Used",Nchunk, "unique bins"
CopyScales/P ctmp, psd
// normalize the sum of PSDs
Variable deltaF= deltax(psd) // deltaF is the frequency bin width
Variable norm= 2 / (npsd * npsd * nsegs * winNorm * deltaF)
psd *= norm
// Explanation of norm values:
// * 2 total power = magnitude^2(-f) + magnitude^2(f), and we've only accumulated magnitude^2(f)
// / (npsd * npsd * nsegs) converts to average power
// / winNorm compensates for power lost by windowing the time data.
// / deltaF converts power to power density (per Hertz)
psd[0] /= 2 // we're not missing 1/2 the power of DC bin from the two-sided FFT, restore original value
psd[npsd/2] /= 2 // there aren't two Nyquist bins, either.
// Parseval's theorem (power in time-domain = power in frequency domain)
// is satisfied if you compare:
// time-domain average power ("mean squared amplitude" in Numerical Recipies) = 1/N * sum(t=0...N-1) w[t]^2
// frequency-domain average power= deltaf * sum(f=0...npsd/2) destw[f]^2
KillWaves/Z tmp, tmp2, win, ctmp, ctmp2
return NameOfWave(psd) // in the current data folder
End
// getNpsd: Given a source wave, computes the number of points to use for the
// 'npsd' parameter in fCrossPowerSpectralDensity to obtain an effective bandwidth
// of bw_res.
// Also, marginally improved to improve resilience against unattainable bandwidth requests.
Function getNpsd(src, bw_res)
WAVE src
Variable bw_res
NVAR/Z quiet
Variable nsrc = numpnts(src)
Variable nBins = (rightX(src) - leftX(src))*bw_res
Variable nPsd = floor(4*(nsrc/(2*nBins)))
Variable roundBins = floor(nBins)
if(roundBins < 1 || nPsd > nsrc)
nPsd = nsrc
if(NVAR_Exists(quiet))
if(quiet == 1)
return nPsd
endif
endif
Print "getNpsd: Requested resolution of", bw_res,"is not possible; using one bin..."
endif
return nPsd
End
Forum
Support
Gallery
Igor Pro 9
Learn More
Igor XOP Toolkit
Learn More
Igor NIDAQ Tools MX
Learn More