0
|
1 |
#======================================================================
|
|
2 |
# F F T . P L
|
|
3 |
# doc: Fri Mar 12 09:20:33 1999
|
|
4 |
# dlm: Mon Jul 24 14:58:04 2006
|
|
5 |
# (c) 1999 A.M. Thurnherr
|
|
6 |
# uE-Info: 241 36 NIL 0 0 72 66 2 4 NIL ofnI
|
|
7 |
#======================================================================
|
|
8 |
|
|
9 |
# Notes:
|
|
10 |
# It was found when rotary-analysing the FLAME current meters that
|
|
11 |
# the sign of the frequencies returned was wrong. When investigating
|
|
12 |
# the problem it was found that there are two conventions, one called
|
|
13 |
# the engineering convention which is followed by Bendat & Piersol
|
|
14 |
# [1971], Gonella [1972], and Mooers [1973] but not Numerical Recipes.
|
|
15 |
# For real-valued functions it does not matter because the power
|
|
16 |
# of the positive and negative frequencies are summed. The engineering
|
|
17 |
# convention appears more sensible, however, because it actually leads
|
|
18 |
# to the anticlockwise component to be reported as positive frequencies
|
|
19 |
# which is consistent with exp(i phi) = cos phi + i sin phi and the
|
|
20 |
# usual axes orientations.
|
|
21 |
|
|
22 |
# HISTORY:
|
|
23 |
# Mar 12, 1999: - adapted from NR
|
|
24 |
# Mar 13, 1999: - ``perlified'' (0-relative arrays; return value)
|
|
25 |
# Mar 14, 1999: - cosmetic changes
|
|
26 |
# Mar 15, 1999: - pad initial/final NaN values with 0es
|
|
27 |
# Dec 08, 1999: - adapted for complex FFT
|
|
28 |
# Dec 09, 1999: - continued
|
|
29 |
# Dec 10, 1999: - BUG: < replaced by <= (no difference)
|
|
30 |
# Dec 11, 1999: - investigated wrong frequency sign
|
|
31 |
# Dec 14, 1999: - changed one-sided spectra to get half of the f=0 power
|
|
32 |
# Mar 04, 2000: - require mean-removal when zero-padding is used
|
|
33 |
# - BUG (cosmetic)
|
|
34 |
# Mar 05: 2000: - BUG: died even if no padding was done
|
|
35 |
# Mar 08, 2000: - removed opt_r hard-coding
|
|
36 |
# May 02, 2001: - BUG: RMS was really standard deviation estimator
|
|
37 |
# Aug 29, 2003: - BUG: &icFFT did not calc sigma correctly whenever
|
|
38 |
# infield == outfield (e.g. [fftfilt] w/o -f)
|
|
39 |
# Mar 31, 2004: - added cFFT_bufR()
|
|
40 |
# Feb 9, 2005: - added &phase_pos(), &phase_neg()
|
|
41 |
# Jul 1, 2006: - Version 3.3 [HISTORY]
|
|
42 |
# Jul 24, 2006: - modified to use $PRACTICALLY_ZERO
|
|
43 |
|
|
44 |
#----------------------------------------------------------------------
|
|
45 |
# FOUR1 routine
|
|
46 |
#
|
|
47 |
# Notes:
|
|
48 |
# - nan will abort FFT!
|
|
49 |
# - added power of two assertions
|
|
50 |
# - arrays are 0-relative
|
|
51 |
# - isig should be set to -1 for FFT and to 1 for reverse FFT (see
|
|
52 |
# note above)
|
|
53 |
#
|
|
54 |
#----------------------------------------------------------------------
|
|
55 |
|
|
56 |
sub FOUR1($@) # ($isign, @data) => @fourier coefficients
|
|
57 |
{
|
|
58 |
my($isign,@data) = @_;
|
|
59 |
my($n,$mmax,$m,$j,$istep,$i);
|
|
60 |
my($wtemp,$wr,$wpr,$wpi,$wi,$theta);
|
|
61 |
my($tempr,$tempi);
|
|
62 |
my($N) = @data / 2;
|
|
63 |
|
|
64 |
$n = $N << 1; # re-order input
|
|
65 |
$j = 0;
|
|
66 |
for ($i=0; $i<$n-1; $i+=2) {
|
|
67 |
if ($j > $i) {
|
|
68 |
$tempr = $data[$j];
|
|
69 |
$tempi = $data[$j+1];
|
|
70 |
$data[$j] = $data[$i];
|
|
71 |
$data[$j+1] = $data[$i+1];
|
|
72 |
$data[$i] = $tempr;
|
|
73 |
$data[$i+1] = $tempi;
|
|
74 |
}
|
|
75 |
croak("$0 (fft.pl) $N is not a power of two\n")
|
|
76 |
if ($n % 2);
|
|
77 |
$m = $n >> 1;
|
|
78 |
while ($m >= 2 && $j >= $m) {
|
|
79 |
$j -= $m;
|
|
80 |
croak("$0 (fft.pl) $N is not a power of two\n")
|
|
81 |
if ($m % 2);
|
|
82 |
$m >>= 1;
|
|
83 |
}
|
|
84 |
$j += $m;
|
|
85 |
}
|
|
86 |
|
|
87 |
# for ($i=0; $i<=$#data; $i+=2) { # dump data
|
|
88 |
# print(STDERR "$data[$i]$opt_O$data[$i+1]$opt_R")
|
|
89 |
# }
|
|
90 |
|
|
91 |
$mmax = 2; # do FFT
|
|
92 |
while ($n > $mmax) {
|
|
93 |
$istep = $mmax << 1;
|
|
94 |
$theta = $isign*(6.28318530717959/$mmax);
|
|
95 |
$wtemp = sin(0.5*$theta);
|
|
96 |
$wpr = -2.0*$wtemp*$wtemp;
|
|
97 |
$wpi = sin($theta);
|
|
98 |
$wr = 1.0;
|
|
99 |
$wi = 0.0;
|
|
100 |
for ($m=0; $m<$mmax-1; $m+=2) {
|
|
101 |
for ($i=$m; $i<=$n-1; $i+=$istep) {
|
|
102 |
$j = $i + $mmax;
|
|
103 |
$tempr = $wr*$data[$j] - $wi*$data[$j+1];
|
|
104 |
$tempi = $wr*$data[$j+1] + $wi*$data[$j];
|
|
105 |
$data[$j] = $data[$i] - $tempr;
|
|
106 |
$data[$j+1] = $data[$i+1] - $tempi;
|
|
107 |
$data[$i] += $tempr;
|
|
108 |
$data[$i+1] += $tempi;
|
|
109 |
}
|
|
110 |
$wtemp = $wr;
|
|
111 |
$wr = $wr*$wpr - $wi*$wpi + $wr;
|
|
112 |
$wi = $wi*$wpr + $wtemp*$wpi + $wi;
|
|
113 |
}
|
|
114 |
$mmax = $istep;
|
|
115 |
}
|
|
116 |
|
|
117 |
return @data;
|
|
118 |
}
|
|
119 |
|
|
120 |
#----------------------------------------------------------------------
|
|
121 |
# TWOFFT routine
|
|
122 |
#
|
|
123 |
# Notes:
|
|
124 |
# - arrays are 0-relative
|
|
125 |
# - array references are used throughout; input arrays are
|
|
126 |
# in $ants_[][] style; ouput are simple arrays
|
|
127 |
# - isign convention of NR is used here!!!
|
|
128 |
#----------------------------------------------------------------------
|
|
129 |
|
|
130 |
sub TWOFFT($$$$$$)
|
|
131 |
{
|
|
132 |
my($data1R,$fnr1,$data2R,$fnr2,$fft1R,$fft2R) = @_;
|
|
133 |
my($nn3,$nn2,$jj,$j);
|
|
134 |
my($rep,$rem,$aip,$aim);
|
|
135 |
my($n) = scalar(@{$data1R});
|
|
136 |
|
|
137 |
$nn3 = 1 + ($nn2 = 2 + $n + $n);
|
|
138 |
for ($j=1,$jj=2; $j<=$n; $j++,$jj+=2) {
|
|
139 |
$fft1R->[$jj-2] = $data1R->[$j-1][$fnr1];
|
|
140 |
$fft1R->[$jj-1] = $data2R->[$j-1][$fnr2];
|
|
141 |
}
|
|
142 |
@{$fft1R} = FOUR1(1,@{$fft1R});
|
|
143 |
$fft2R->[0] = $fft1R->[1];
|
|
144 |
$fft1R->[1] = $fft2R->[1] = 0;
|
|
145 |
for ($j=3; $j<=$n+1; $j+=2) {
|
|
146 |
$rep = 0.5 * ($fft1R->[$j-1] + $fft1R->[$nn2-$j-1]);
|
|
147 |
$rem = 0.5 * ($fft1R->[$j-1] - $fft1R->[$nn2-$j-1]);
|
|
148 |
$aip = 0.5 * ($fft1R->[$j] + $fft1R->[$nn3-$j-1]);
|
|
149 |
$aim = 0.5 * ($fft1R->[$j] - $fft1R->[$nn3-$j-1]);
|
|
150 |
$fft1R->[$j-1] = $rep;
|
|
151 |
$fft1R->[$j] = $aim;
|
|
152 |
$fft1R->[$nn2-$j-1] = $rep;
|
|
153 |
$fft1R->[$nn3-$j-1] = -$aim;
|
|
154 |
$fft2R->[$j-1] = $aip;
|
|
155 |
$fft2R->[$j] = -$rem;
|
|
156 |
$fft2R->[$nn2-$j-1] = $aip;
|
|
157 |
$fft2R->[$nn3-$j-1] = $rem;
|
|
158 |
}
|
|
159 |
}
|
|
160 |
|
|
161 |
#----------------------------------------------------------------------
|
|
162 |
# Interface to @ants_
|
|
163 |
#
|
|
164 |
# Notes:
|
|
165 |
# - N (number of complex samples) calculated as next larger pwr-of-two
|
|
166 |
# if set to 0 on calling; otherwise set is used
|
|
167 |
# - @ants_ padded with 0es to $N (deprecated in Hamming [1989])
|
|
168 |
# - ditto initial and final missing values
|
|
169 |
# - set ifnr to nan if input is purely real
|
|
170 |
#
|
|
171 |
#----------------------------------------------------------------------
|
|
172 |
|
|
173 |
sub cFFT($$$) { return cFFT_bufR(\@ants_,@_); }
|
|
174 |
|
|
175 |
sub cFFT_bufR($$$) # ($bufR, $rfnr, $ifnr, [$N]) => @coeff
|
|
176 |
{
|
|
177 |
my($bufR,$fnr,$ifnr,$N) = @_;
|
|
178 |
my(@data,$i,$lastSet);
|
|
179 |
|
|
180 |
unless ($N) { # $N not set
|
|
181 |
for ($N=1; $N <= $#ants_; $N <<= 1) {} # next greater pwroftwo
|
|
182 |
&antsInfo("(fft.pl) N set to $N")
|
|
183 |
unless ($N == $#ants_+1);
|
|
184 |
}
|
|
185 |
for ($i=0; $i<$N && $i<=$#ants_; $i++) { # PAD
|
|
186 |
last if (numberp($bufR->[$i][$fnr]) &&
|
|
187 |
(isnan($ifnr) || numberp($bufR->[$i][$ifnr])));
|
|
188 |
$data[2*$i] = 0;
|
|
189 |
$data[2*$i+1] = 0;
|
|
190 |
}
|
|
191 |
$lastSet = $i - 1;
|
|
192 |
&antsInfo("(fft.pl) WARNING: $i initial non-numbers padded with 0es!!!"),
|
|
193 |
$padded=1 if ($i);
|
|
194 |
while ($i<$N && $i<=$#ants_) { # fill
|
|
195 |
$i++,next unless (numberp($bufR->[$i][$fnr]) && # skip non-numbers
|
|
196 |
(isnan($ifnr) || numberp($bufR->[$i][$ifnr])));
|
|
197 |
croak("$0: (fft.pl) $lastSet, $i can't handle missing values ($bufR->[$lastSet+1][$fnr])!\n")
|
|
198 |
if ($lastSet != $i-1); # missing values
|
|
199 |
$data[2*$i] = $bufR->[$i][$fnr]; # real
|
|
200 |
$data[2*$i+1] = isnan($ifnr) ? 0 : $bufR->[$i][$ifnr]; # imag
|
|
201 |
$lastSet = $i;
|
|
202 |
$i++;
|
|
203 |
}
|
|
204 |
&antsInfo("(fft.pl) WARNING: %d final non-numbers padded with 0es!!!",$i-$lastSet-1),
|
|
205 |
$padded=1 if ($i > $lastSet+1);
|
|
206 |
&antsInfo("(fft.pl) WARNING: padded with %d 0es to next pwr-of-two!!!",$N-$i),
|
|
207 |
$padded=1 if ($i < $N);
|
|
208 |
croak("$0: (fft.pl) refusing to pad with zeroes unless mean is removed (sorry)\n")
|
|
209 |
if ($padded && !$FFT_ALLOW_ZERO_PADDING);
|
|
210 |
$i = $lastSet + 1;
|
|
211 |
while ($i < $N) { # PAD
|
|
212 |
$data[2*$i] = 0;
|
|
213 |
$data[2*$i+1] = 0;
|
|
214 |
$i++;
|
|
215 |
}
|
|
216 |
return &FOUR1(-1,@data);
|
|
217 |
}
|
|
218 |
|
|
219 |
sub icFFT($$@) # ($ofnr, $tfnr, @coeff) => sigma
|
|
220 |
{
|
|
221 |
my($ofnr,$tfnr,@coeff) = @_;
|
|
222 |
my($N) = ($#coeff+1)/2;
|
|
223 |
my(@val) = &FOUR1(1,@coeff);
|
|
224 |
my($i);
|
|
225 |
my($n) = 0;
|
|
226 |
my($sumsq) = 0;
|
|
227 |
my($mai) = 0;
|
|
228 |
|
|
229 |
for ($i=0; $i<$N && $i<=$#ants_ ; $i++) { # fill
|
|
230 |
my($oldval) = $ants_[$i][$ofnr];
|
|
231 |
push(@{$ants_[$i]},nan) # pad empty fields
|
|
232 |
while ($#{$ants_[$i]} < $tfnr);
|
|
233 |
$ants_[$i][$tfnr] = $val[2*$i]/$N; # real
|
|
234 |
$mai = abs($val[2*$i+1]) # imag
|
|
235 |
if (abs($val[2*$i+1]) > $mai);
|
|
236 |
next unless (numberp($oldval)); # sigma
|
|
237 |
$sumsq += ($oldval - $ants_[$i][$tfnr])**2;
|
|
238 |
$n++;
|
|
239 |
}
|
|
240 |
&antsInfo("(fft.pl) WARNING: imaginary exponents (abs <= $mai) ignored")
|
|
241 |
if ($mai > $PRACTICALLY_ZERO);
|
|
242 |
&antsInfo("(fft.pl) WARNING: reducing data (%d -> %d)",$#ants_+1,$N)
|
|
243 |
if ($i <= $#ants_);
|
|
244 |
while ($i <= $#ants_) {
|
|
245 |
$ants_[$i][$fnr] = nan;
|
|
246 |
$i++;
|
|
247 |
}
|
|
248 |
return ($n > 1) ? sqrt($sumsq/($n-1)) : nan;
|
|
249 |
}
|
|
250 |
|
|
251 |
#----------------------------------------------------------------------
|
|
252 |
# Periodogram (p.421; (12.7.5) -- (12.7.6))
|
|
253 |
#----------------------------------------------------------------------
|
|
254 |
|
|
255 |
# Miscellaneous Notes:
|
|
256 |
#
|
|
257 |
# - there are N/2 + 1 values in the (unbinned) PSD (see NR)
|
|
258 |
|
|
259 |
# Notes regarding the effects of zero padding:
|
|
260 |
#
|
|
261 |
# - using zero-padding on a time series where the mean is not removed
|
|
262 |
# can result in TOTALLY DIFFERENT RESULTS, try it e.g. with
|
|
263 |
# temperatures!!! (A moment's thought will reveal why)
|
|
264 |
#
|
|
265 |
# - Because the total power is normalized to the mean squared amplitude
|
|
266 |
# 0-padded values depress the power; this was taken care of below by
|
|
267 |
# normalizing the power by multiplying it with nrm=(nData+nZeroes)/nData;
|
|
268 |
#
|
|
269 |
# - this was checked using `avg -m' (in case of complex input the total
|
|
270 |
# power is given by sqrt(Re**2+Im**2));
|
|
271 |
#
|
|
272 |
# - if zero-padding is used sqrt(nrm*P[0]) is mean value (done in [pgram])
|
|
273 |
|
|
274 |
# Notes on interpreting the power spectrum (Hamming [1989] & NR):
|
|
275 |
#
|
|
276 |
# - the frequency spectrum encompasses the frequencies between 0 (the
|
|
277 |
# mean value) and the Nyquist frequency (1 / (2 x sampling interval))
|
|
278 |
#
|
|
279 |
# - higher frequencies are aliased into the power spectrum in a mirrored
|
|
280 |
# way (e.g. noise tends to (linearly?) approach zero as f goes to inft;
|
|
281 |
# the downsloping spectrum `hits' the Nyquist frequency, turns around
|
|
282 |
# and continues falling towards the zero frequency, where it gets mirrored
|
|
283 |
# again => spectrum flattens towards Nyquist frequency
|
|
284 |
#
|
|
285 |
# - the sum over all P's (total power) is equal to the mean square value;
|
|
286 |
# when one-sided spectra are used, P[0] and P[N/2] are counted doubly
|
|
287 |
# and must be subtracted from the total; NB: the total power is reduced
|
|
288 |
# if data are padded with 0es
|
|
289 |
#
|
|
290 |
# - sqrt(P[0]) is an estimate for the mean value which is only accurate
|
|
291 |
# if no zero-padding is perfomed; removing the mean will
|
|
292 |
# strongly change the spectrum near the origin which might
|
|
293 |
# or might not be a good thing, depending on the physics behind it (e.g.
|
|
294 |
# it makes sense to remove the mean if a power spectrum from a temperature
|
|
295 |
# record is calculated but not if flow velocity is used).
|
|
296 |
# Removing higher order trends will also affect the spectrum but not
|
|
297 |
# in such a simple fashion. Note that the problem is mainly restricted
|
|
298 |
# to cases where the signal is in the low frequency and thus affected
|
|
299 |
# by the strong changes in the spectrum there.
|
|
300 |
|
|
301 |
# Notes on the two-sided spectra:
|
|
302 |
# - the power of the mean flow (sqrt(P[0])) is non-rotary. To have the sum
|
|
303 |
# of both one-sided spectra equal the two-sided one, each one-sided
|
|
304 |
# spectrum gets half of the total value.
|
|
305 |
# - the same is true for the highest frequency; at the Nyquist frequency
|
|
306 |
# every rotation is sampled exactly twice => polarization cannot be
|
|
307 |
# determined (imagine a wheel with one spoke...)
|
|
308 |
|
|
309 |
|
|
310 |
sub pgram_onesided(@) # $nData,@C -> return @P
|
|
311 |
{
|
|
312 |
my($nData,@C) = @_;
|
|
313 |
my($N) = ($#C+1) / 2; # number of fourier comps
|
|
314 |
my($Pfac) = $N**(-2) * $N/$nData; # normalized to mean-sq amp
|
|
315 |
my($k,@P);
|
|
316 |
|
|
317 |
$P[0] = $Pfac * ($C[0]**2 + $C[1]**2); # calc periodogram
|
|
318 |
for ($k=1; $k<=$N/2-1; $k++) {
|
|
319 |
$P[$k] = $Pfac * ($C[2*$k]**2 + $C[2*$k+1]**2 +
|
|
320 |
$C[2*($N-$k)]**2 + $C[2*($N-$k)+1]**2);
|
|
321 |
}
|
|
322 |
$P[$N/2] = $Pfac * ($C[2*($N/2)]**2 + $C[2*($N/2)+1]**2);
|
|
323 |
return @P;
|
|
324 |
}
|
|
325 |
|
|
326 |
sub pgram_pos(@) # $nData,@C -> return @P
|
|
327 |
{
|
|
328 |
my($nData,@C) = @_;
|
|
329 |
my($N) = ($#C+1) / 2; # number of fourier comps
|
|
330 |
my($Pfac) = $N**(-2) * $N/$nData; # normalized to mean-sq amp
|
|
331 |
my($k,@P);
|
|
332 |
|
|
333 |
$P[0] = 0.5 * $Pfac * ($C[0]**2 + $C[1]**2); # calc periodogram
|
|
334 |
for ($k=1; $k<=$N/2-1; $k++) {
|
|
335 |
$P[$k] = $Pfac * ($C[2*$k]**2 + $C[2*$k+1]**2);
|
|
336 |
}
|
|
337 |
$P[$N/2] = 0.5 * $Pfac * ($C[2*($N/2)]**2 + $C[2*($N/2)+1]**2);
|
|
338 |
return @P;
|
|
339 |
}
|
|
340 |
|
|
341 |
sub pgram_neg(@) # $nData,@C -> return @P
|
|
342 |
{
|
|
343 |
my($nData,@C) = @_;
|
|
344 |
my($N) = ($#C+1) / 2; # number of fourier comps
|
|
345 |
my($Pfac) = $N**(-2) * $N/$nData; # normalized to mean-sq amp
|
|
346 |
my($k,@P);
|
|
347 |
|
|
348 |
$P[0] = 0.5 * $Pfac * ($C[0]**2 + $C[1]**2); # calc periodogram
|
|
349 |
for ($k=1; $k<=$N/2-1; $k++) {
|
|
350 |
$P[$k] = $Pfac * ($C[2*($N-$k)]**2 + $C[2*($N-$k)+1]**2);
|
|
351 |
}
|
|
352 |
$P[$N/2] = 0.5 * $Pfac * ($C[2*($N/2)]**2 + $C[2*($N/2)+1]**2);
|
|
353 |
return @P;
|
|
354 |
}
|
|
355 |
|
|
356 |
#------------------------------------------------------------------------
|
|
357 |
# Current Ellipses (Emery and Thomson, 5.6.4.2 (Rotary Component Spectra)
|
|
358 |
#------------------------------------------------------------------------
|
|
359 |
|
|
360 |
# Notes on phase (ellipsis inclination) calculations:
|
|
361 |
# - comparing equations 5.6.42 and 5.6.43 in E+T shows a sign
|
|
362 |
# sign change of one of the terms (U_2k - V_1k in eqn 5.6.42a)
|
|
363 |
# - for reasons of symmetry, this is unlikely to be correct
|
|
364 |
# - in order to test this, I compared the M2 ellipses inclinations of
|
|
365 |
# a tidal analysis of BBTRE instruments #1 & #3 with the output
|
|
366 |
# and found that changing the sign in 5.6.43a brought the values
|
|
367 |
# into agreement (#1: tidal analysis 155+-3, pgram -25; #3: 136+-5,
|
|
368 |
# pgram -44)
|
|
369 |
# - the changes are marked in the code by a (superfluous) + sign
|
|
370 |
# - the sign change was later found to be consistent with the
|
|
371 |
# tidal analysis package manual by MGG Foreman (p.11)
|
|
372 |
|
|
373 |
sub phase_pos(@) # @C -> return @eps
|
|
374 |
{
|
|
375 |
my(@C) = @_;
|
|
376 |
my($N) = ($#C+1) / 2; # number of fourier comps
|
|
377 |
my($k,@eps);
|
|
378 |
|
|
379 |
$eps[0] = 180 / $PI * atan2(+$C[1],$C[0]);
|
|
380 |
for ($k=1; $k<=$N/2-1; $k++) {
|
|
381 |
$eps[$k] = 180 / $PI * atan2(+$C[2*$k+1],$C[2*$k]);
|
|
382 |
}
|
|
383 |
$eps[$N/2] = 180 / $PI * atan2(+$C[2*($N/2)+1],$C[2*($N/2)]);
|
|
384 |
return @eps;
|
|
385 |
}
|
|
386 |
|
|
387 |
sub phase_neg(@) # @C -> return @P
|
|
388 |
{
|
|
389 |
my(@C) = @_;
|
|
390 |
my($N) = ($#C+1) / 2; # number of fourier comps
|
|
391 |
my($k,@P);
|
|
392 |
|
|
393 |
$eps[0] = 180 / $PI * atan2(+$C[1],$C[0]);
|
|
394 |
for ($k=1; $k<=$N/2-1; $k++) {
|
|
395 |
$eps[$k] = 180 / $PI * atan2(+$C[2*($N-$k)+1],$C[2*($N-$k)]);
|
|
396 |
}
|
|
397 |
$eps[$N/2] = 180 / $PI * atan2(+$C[2*($N/2)+1],$C[2*($N/2)]);
|
|
398 |
return @eps;
|
|
399 |
}
|
|
400 |
|
|
401 |
#----------------------------------------------------------------------
|
|
402 |
|
|
403 |
1;
|