mxlib
c++ tools for analyzing astronomical data and other tasks by Jared R. Males. [git repo]
Loading...
Searching...
No Matches
psdUtils.hpp
Go to the documentation of this file.
1/** \file psdUtils.hpp
2 * \brief Tools for working with PSDs
3 *
4 * \author Jared R. Males (jaredmales@gmail.com)
5 *
6 * \ingroup signal_processing_files
7 *
8 */
9
10//***********************************************************************//
11// Copyright 2015-2021 Jared R. Males (jaredmales@gmail.com)
12//
13// This file is part of mxlib.
14//
15// mxlib is free software: you can redistribute it and/or modify
16// it under the terms of the GNU General Public License as published by
17// the Free Software Foundation, either version 3 of the License, or
18// (at your option) any later version.
19//
20// mxlib is distributed in the hope that it will be useful,
21// but WITHOUT ANY WARRANTY; without even the implied warranty of
22// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23// GNU General Public License for more details.
24//
25// You should have received a copy of the GNU General Public License
26// along with mxlib. If not, see <http://www.gnu.org/licenses/>.
27//***********************************************************************//
28
29#ifndef psdUtils_hpp
30#define psdUtils_hpp
31
32#ifndef EIGEN_NO_CUDA
33 #define EIGEN_NO_CUDA
34#endif
35
36#include <type_traits>
37
38#include <Eigen/Dense>
39
40#include <iostream>
41
42#include "../mxlib.hpp"
43
44#include "../math/ft/fftT.hpp"
46
47namespace mx
48{
49namespace sigproc
50{
51
52/** \ingroup psds
53 * @{
54 */
55
56/// Calculate the variance of a 1-D, 1-sided PSD
57/** By default uses trapezoid rule integration. This can be changed to mid-point integration.
58 *
59 * \returns the variance of a PSD (the integral).
60 *
61 * \tparam realT the real floating point type
62 *
63 */
64template <typename realT>
65realT psdVar1sided( realT df, ///< [in] the frequency scale of the PSD
66 const realT *PSD, ///< [in] the PSD to integrate.
67 size_t sz, ///< [in] the size of the PSD vector
68 realT half = 0.5 ///< [in] [optional] controls if trapezoid (0.5) or mid-point (1.0) integration is
69 ///< used. Do not use other values.
70)
71{
72 realT var = 0;
73
74 var = half * PSD[0];
75
76 for( size_t i = 1; i < sz - 1; ++i )
77 {
78 var += PSD[i];
79 }
80
81 var += half * PSD[sz - 1];
82
83 var *= df;
84
85 return var;
86}
87
88/// Calculate the variance of a 1-D, 2-sided PSD
89/** By default uses trapezoid rule integration. This can be changed to mid-point integration.
90 *
91 * Assumes the 2-sided PSD is in standard FFT storage order, and that sz is even.
92 *
93 * \returns the variance of a PSD (the integral).
94 *
95 * \tparam realT the real floating point type
96 *
97 */
98template <typename realT>
99realT psdVar2sided( realT df, ///< [in] the frequency scale of the PSD
100 const realT *PSD, ///< [in] the PSD to integrate.
101 size_t sz, ///< [in] the size of the PSD vector
102 realT half = 0.5 ///< [in] [optional] controls if trapezoid (0.5) or mid-point (1.0) integration is
103 ///< used. Do not use other values.
104)
105{
106 realT var = 0;
107
108 var = PSD[0];
109
110 size_t i = 1;
111 for( ; i < sz / 2; ++i )
112 {
113 var += PSD[i];
114 var += PSD[sz - i];
115 }
116 var += half * PSD[i]; // The mid-point is double. It is also the endpoint of integration from each side, so it
117 // would enter twice, hence once here.
118
119 var *= df;
120
121 return var;
122}
123
124/// Calculate the variance of a 1-D PSD
125/** By default uses trapezoid rule integration. This can be changed to mid-point integration.
126 *
127 * If f.back() < 0, then a 2-sided PSD in FFT storage order is assumed. Otherwise, PSD is treated as 1-sided.
128 *
129 * \returns the variance of a PSD (the integral).
130 *
131 * \tparam realT the real floating point type
132 *
133 */
134template <typename realT>
135realT psdVar( const std::vector<realT> &f, ///< [in] the frequency scale of the PSD.
136 const std::vector<realT> &PSD, ///< [in] the PSD to integrate.
137 realT half = 0.5 ///< [in] [optional] controls if trapezoid (0.5) or mid-point (1.0) integration is used.
138 ///< Do not use other values.
139)
140{
141 if( f.back() < 0 )
142 {
143 return psdVar2sided( f[1] - f[0], PSD.data(), PSD.size(), half );
144 }
145 else
146 {
147 return psdVar1sided( f[1] - f[0], PSD.data(), PSD.size(), half );
148 }
149}
150
151/// Calculate the variance of a PSD
152/** By default uses trapezoid rule integration. This can be changed to mid-point integration.
153 *
154 * \overload
155 *
156 * \returns the variance of a PSD (the integral).
157 *
158 * \tparam realT the real floating point type
159 */
160template <typename eigenArrT>
161typename eigenArrT::Scalar psdVarDisabled(
162 eigenArrT &freq, ///< [in] the frequency scale of the PSD
163 eigenArrT &PSD, ///< [in] the PSD to integrate.
164 bool trap = true ///< [in] [optional] controls if trapezoid (true) or mid-point (false) integration is used.
165)
166{
167 typename eigenArrT::Scalar half = 0.5;
168 if( !trap )
169 half = 1.0;
170
171 typename eigenArrT::Scalar var = 0;
172
173 var = half * PSD( 0, 0 );
174
175 for( int i = 1; i < freq.rows() - 1; ++i )
176 var += PSD( i, 0 );
177
178 var += half * PSD( freq.rows() - 1, 0 );
179
180 var *= ( freq( 1, 0 ) - freq( 0, 0 ) );
181
182 return var;
183}
184
185/// Calculates the frequency sampling for a grid given maximum dimension and maximum frequency.
186/** The freq_sampling is
187 * @f$ \Delta f = f_{max}/ (0.5*dim) @f$
188 * where @f$ f_{max} = 1/(2\Delta t) @f$ is the maximum frequency and @f$ dim @f$ is the size of the grid.
189 *
190 * \param [in] dim is the size of the grid
191 * \param [in] f_max is the maximum frequency of the grid
192 *
193 * \returns the sampling interval @f$ \delta f @f$
194 *
195 * \tparam realT is the real floating point type used for calculations.
196 *
197 */
198template <class realT>
199realT freq_sampling( size_t dim, realT f_max )
200{
201 return ( f_max / ( 0.5 * dim ) );
202}
203
204#if 0
205///Create a 1-D frequency grid
206/**
207 * \param [out] vec the pre-allocated Eigen-type 1xN or Nx1 array, on return contains the frequency grid
208 * \param [in] dt the temporal sampling of the time series
209 * \param [in] inverse [optional] if true
210 *
211 * \tparam eigenArr the Eigen-like array type
212 */
213template<typename eigenArr>
214void frequency_grid1D( eigenArr & vec,
215 typename eigenArr::Scalar dt,
216 bool inverse = false )
217{
218 typename eigenArr::Index dim, dim_1, dim_2;
219 typename eigenArr::Scalar df;
220
221 dim_1 = vec.rows();
222 dim_2 = vec.cols();
223
224 dim = std::max(dim_1, dim_2);
225
226 df = freq_sampling(dim, 0.5/dt);
227
228 if( !inverse )
229 {
230 for(int ii=0; ii < ceil(0.5*(dim-1) + 1); ++ii)
231 {
232 vec(ii) = ii*df;
233 }
234
235 for(int ii=ceil(0.5*(dim-1)+1); ii < dim_1; ++ii)
236 {
237 vec(ii) = (ii-dim)*df;
238 }
239 }
240 else
241 {
242 for(int ii=0; ii < dim; ++ii)
243 {
244 vec(ii) = df * ii / dim;
245 }
246 }
247}
248#endif
249
250/// Create a 1-D frequency grid
251/**
252 *
253 * \tparam realT a real floating point type
254 * \tparam realParamT a real floating point type, convenience to avoid double-float confusion.
255 *
256 */
257template <typename realT, typename realParamT>
259 std::vector<realT> &vec, ///< [out] vec the pre-allocated vector, on return contains the frequency grid
260 realParamT dt, ///< [in] dt the temporal sampling of the time series
261 bool fftOrder = true ///< [in] fftOrder [optional] if true the frequency grid is in FFT order
262)
263{
264 realT dtTT = dt;
265
266 if( fftOrder )
267 {
268 if( vec.size() % 2 == 1 )
269 {
270 internal::mxlib_error_report(error_t::invalidarg,"Frequency scale can't be odd-sized for FFT order" );
271 return -1;
272 }
273
274 realT df = ( 1.0 / dtTT ) / ( (realT)vec.size() );
275
276 for( ssize_t ii = 0; ii < ceil( 0.5 * ( vec.size() - 1 ) + 1 ); ++ii )
277 {
278 vec[ii] = ii * df;
279 }
280
281 for( ssize_t ii = ceil( 0.5 * ( vec.size() - 1 ) + 1 ); ii < vec.size(); ++ii )
282 {
283 vec[ii] = ( ii - (ssize_t)vec.size() ) * df;
284 }
285
286 return 0;
287 }
288 else
289 {
290 if( vec.size() % 2 == 0 )
291 {
292 realT df = ( 0.5 / dtTT ) / ( (realT)vec.size() - 1 );
293 for( int ii = 0; ii < vec.size(); ++ii )
294 {
295 vec[ii] = df * ii;
296 }
297
298 return 0;
299 }
300 else
301 {
302 realT df = ( 0.5 / dt ) / ( (realT)vec.size() );
303 for( int ii = 0; ii < vec.size(); ++ii )
304 {
305 vec[ii] = df * ( ii + 1 );
306 }
307
308 return 0;
309 }
310 }
311}
312
313/// Create a 2-D frequency grid
314template <typename eigenArr, typename realParamT>
315void frequencyGrid( eigenArr &arr, realParamT drT, eigenArr *k_x, eigenArr *k_y )
316{
317 typename eigenArr::Scalar dr = drT;
318
319 typename eigenArr::Index dim_1, dim_2;
320 typename eigenArr::Scalar k_1, k_2, df;
321
322 dim_1 = arr.rows();
323 dim_2 = arr.cols();
324
325 if( k_x )
326 k_x->resize( dim_1, dim_2 );
327 if( k_y )
328 k_y->resize( dim_1, dim_2 );
329
330 df = freq_sampling( std::max( dim_1, dim_2 ), 0.5 / dr );
331
332 for( int ii = 0; ii < 0.5 * ( dim_1 - 1 ) + 1; ++ii )
333 {
334 k_1 = ii * df;
335 for( int jj = 0; jj < 0.5 * ( dim_2 - 1 ) + 1; ++jj )
336 {
337 k_2 = jj * df;
338
339 arr( ii, jj ) = sqrt( k_1 * k_1 + k_2 * k_2 );
340
341 if( k_x )
342 ( *k_x )( ii, jj ) = k_1;
343 if( k_x )
344 ( *k_y )( ii, jj ) = k_2;
345 }
346
347 for( int jj = 0.5 * ( dim_2 - 1 ) + 1; jj < dim_2; ++jj )
348 {
349 k_2 = ( jj - dim_2 ) * df;
350
351 arr( ii, jj ) = sqrt( k_1 * k_1 + k_2 * k_2 );
352
353 if( k_x )
354 ( *k_x )( ii, jj ) = k_1;
355 if( k_x )
356 ( *k_y )( ii, jj ) = k_2;
357 }
358 }
359
360 for( int ii = 0.5 * ( dim_1 - 1 ) + 1; ii < dim_1; ++ii )
361 {
362 k_1 = ( ii - dim_1 ) * df;
363 for( int jj = 0; jj < 0.5 * ( dim_2 - 1 ) + 1; ++jj )
364 {
365 k_2 = jj * df;
366
367 arr( ii, jj ) = sqrt( k_1 * k_1 + k_2 * k_2 );
368
369 if( k_x )
370 ( *k_x )( ii, jj ) = k_1;
371 if( k_x )
372 ( *k_y )( ii, jj ) = k_2;
373 }
374
375 for( int jj = 0.5 * ( dim_2 - 1 ) + 1; jj < dim_2; ++jj )
376 {
377 k_2 = ( jj - dim_2 ) * df;
378
379 arr( ii, jj ) = sqrt( k_1 * k_1 + k_2 * k_2 );
380
381 if( k_x )
382 ( *k_x )( ii, jj ) = k_1;
383 if( k_x )
384 ( *k_y )( ii, jj ) = k_2;
385 }
386 }
387}
388
389/// Create a frequency grid
390template <typename eigenArr>
391void frequencyGrid( eigenArr &arr, typename eigenArr::Scalar dt )
392{
393 frequencyGrid( arr, dt, (eigenArr *)0, (eigenArr *)0 );
394}
395
396/// Create a frequency grid
397template <typename eigenArr>
398void frequencyGrid( eigenArr &arr, typename eigenArr::Scalar dt, eigenArr &k_x, eigenArr &k_y )
399{
400 frequencyGrid( arr, dt, &k_x, &k_y );
401}
402
403/// Calculate the normalization for a 1-D @f$ 1/|f|^\alpha @f$ PSD.
404/**
405 * \param [in] fmin is the minimum non-zero absolute value of frequency
406 * \param [in] fmax is the maximum absolute value of frequencey
407 * \param [in] alpha is the power-law exponent, by convention @f$ \alpha > 0 @f$.
408 *
409 * \returns the normalization for a 2-sided power law PSD.
410 *
411 * \tparam realT is the real floating point type used for calculations.
412 */
413template <typename realT>
414realT oneoverf_norm( realT fmin, realT fmax, realT alpha )
415{
416 realT integ = 2 * ( pow( fmax, -1.0 * alpha + 1.0 ) - pow( fmin, -1.0 * alpha + 1.0 ) ) / ( -1.0 * alpha + 1.0 );
417
418 return 1 / integ;
419}
420
421/// Calculate the normalization for a 2-D @f$ 1/|k|^\alpha @f$ PSD.
422/**
423 * \param [in] kmin is the minimum non-zero absolute value of frequency
424 * \param [in] kmax is the maximum absolute value of frequencey
425 * \param [in] alpha is the power-law exponent, by convention @f$ \alpha > 0 @f$.
426 *
427 * \returns the normalization for a 2-D, 2-sided power law PSD.
428 *
429 * \tparam realT is the real floating point type used for calculations.
430 */
431template <typename realT>
432realT oneoverk_norm( realT kmin, realT kmax, realT alpha )
433{
434 realT integ = 2 * ( pow( kmax, -1 * alpha + 2.0 ) - pow( kmin, -1.0 * alpha + 2.0 ) ) / ( -1 * alpha + 2.0 );
435
436 return 1 / integ;
437}
438
439/// Normalize a 1-D PSD to have a given variance
440/** A frequency range can be specified to calculate the norm, otherwise f[0] to f[f.size()-1] is the range. The entire
441 * PSD is normalized regardless.
442 *
443 * \tparam floatT the floating point type of the PSD.
444 * \tparam floatParamT a floating point type, convenience to avoid double-float cofusion.
445 *
446 */
447template <typename floatT, typename floatParamT>
448int normPSD( std::vector<floatT> &psd, ///< [in.out] the PSD to normalize, will be altered.
449 std::vector<floatT> &f, ///< [in] the frequency points for the PSD
450 floatParamT normT, ///< [in] the desired total variance (or integral) of the PSD.
451 floatT fmin = std::numeric_limits<floatT>::min(), ///< [in] [optiona] the minimum frequency of the range
452 ///< over which to normalize.
453 floatT fmax = std::numeric_limits<floatT>::max() ///< [in] [optiona] the maximum frequency of the range
454 ///< over which to normalize.
455)
456{
457 floatT norm = normT;
458
459 floatT s = 0; // accumulate
460
461 // Check if inside-the-loop branch is needed
462 if( fmin != std::numeric_limits<floatT>::min() || fmax != std::numeric_limits<floatT>::max() )
463 {
464 for( size_t i = 0; i < psd.size(); ++i )
465 {
466 if( fabs( f[i] ) < fmin || fabs( f[i] ) > fmax )
467 continue;
468 s += psd[i];
469 }
470 }
471 else
472 {
473 for( size_t i = 0; i < psd.size(); ++i )
474 {
475 s += psd[i];
476 }
477 }
478
479 s *= ( f[1] - f[0] );
480
481 for( size_t i = 0; i < psd.size(); ++i )
482 psd[i] *= norm / s;
483
484 return 0;
485}
486
487/// Normalize a 2-D PSD to have a given variance
488/** A frequency range can be specified for calculating the norm, otherwise the entire PSD is used. The entire PSD is
489 * normalized regardless.
490 *
491 * \tparam floatT the floating point type of the PSD.
492 * \tparam floatParamT a floating point type, convenience to avoid double-float cofusion.
493 *
494 */
495template <typename floatT, typename floatParamT>
496floatT
497normPSD( Eigen::Array<floatT, Eigen::Dynamic, Eigen::Dynamic> &psd, ///< [in.out] the PSD to normalize, will be altered.
498 Eigen::Array<floatT, Eigen::Dynamic, Eigen::Dynamic> &k, ///< [in] the frequency grid for psd.
499 floatParamT normT, ///< [in] the desired total variance (or integral) of the PSD.
500 floatT kmin = std::numeric_limits<floatT>::min(), ///< [in] [optiona] the minimum frequency of the range over
501 ///< which to normalize.
502 floatT kmax = std::numeric_limits<floatT>::max() ///< [in] [optiona] the maximum frequency of the range over
503 ///< which to normalize.
504)
505{
506 floatT norm = normT;
507
508 floatT dk1, dk2;
509
510 if( k.rows() > 1 )
511 dk1 = k( 1, 0 ) - k( 0, 0 );
512 else
513 dk1 = 1;
514
515 if( k.cols() > 1 )
516 dk2 = k( 0, 1 ) - k( 0, 0 );
517 else
518 dk2 = 1;
519
520 floatT s = 0;
521
522 // Check if inside-the-loop branch is needed
523 if( kmin != std::numeric_limits<floatT>::min() || kmax != std::numeric_limits<floatT>::max() )
524 {
525 for( int c = 0; c < psd.cols(); ++c )
526 {
527 for( int r = 0; r < psd.rows(); ++r )
528 {
529 if( fabs( k( r, c ) ) < kmin || fabs( k( r, c ) ) > kmax )
530 continue;
531 s += psd( r, c );
532 }
533 }
534 }
535 else
536 {
537 for( int c = 0; c < psd.cols(); ++c )
538 {
539 for( int r = 0; r < psd.rows(); ++r )
540 {
541 s += psd( r, c );
542 }
543 }
544 }
545
546 s *= dk1 * dk2;
547
548 for( int c = 0; c < psd.cols(); ++c )
549 {
550 for( int r = 0; r < psd.rows(); ++r )
551 {
552 psd( r, c ) *= norm / s;
553 }
554 }
555
556 return 0;
557}
558
559/// Generates a @f$ 1/|f|^\alpha @f$ power spectrum
560/**
561 * Populates an Eigen array with
562 * \f[
563 * P(|f| = 0) = 0
564 * \f]
565 * \f[
566 * P(|f| > 0) = \frac{\beta}{|f|^{\alpha}}
567 * \f]
568 *
569 *
570 * \param [out] psd is the array to populate
571 * \param [in] freq is a frequency grid, must be the same logical size as psd
572 * \param [in] alpha is the power law exponent, by convention @f$ alpha > 0 @f$.
573 * \param [in] beta [optional is a normalization constant to multiply the raw spectrum by. If beta==-1 (default) then
574 * the PSD is normalized using \ref oneoverf_norm.
575 *
576 * \tparam eigenArrp is the Eigen-like array type of the psd
577 * \tparam eigenArrf is the Eigen-like array type of the frequency grid
578 */
579template <typename eigenArrp, typename eigenArrf>
580void oneoverf_psd( eigenArrp &psd,
581 eigenArrf &freq,
582 typename eigenArrp::Scalar alpha,
583 typename eigenArrp::Scalar beta = -1 )
584{
585 typedef typename eigenArrp::Scalar Scalar;
586
587 typename eigenArrp::Index dim_1, dim_2;
588 Scalar f_x, f_y, p;
589
590 dim_1 = psd.rows();
591 dim_2 = psd.cols();
592
593 if( beta == -1 )
594 {
595 Scalar fmin;
596 Scalar fmax;
597
598 fmax = freq.abs().maxCoeff();
599
600 // Find minimum non-zero Coeff.
601 fmin = ( freq.abs() > 0 ).select( freq.abs(), freq.abs() + fmax ).minCoeff();
602
603 beta = oneoverf_norm( fmin, fmax, alpha );
604 }
605
606 for( int ii = 0; ii < dim_1; ++ii )
607 {
608 for( int jj = 0; jj < dim_2; ++jj )
609 {
610 if( freq( ii, jj ) == 0 )
611 {
612 p = 0;
613 }
614 else
615 {
616 p = beta / std::pow( std::abs( freq( ii, jj ) ), alpha );
617 }
618 psd( ii, jj ) = p;
619 }
620 }
621}
622
623/// Generate a 1-D von Karman power spectrum
624/**
625 * Populates an Eigen array with
626 *
627 * \f[
628 * P(f) = \frac{\beta}{ (f^2 + (1/T_0)^2)^{\alpha/2}} e^{ - f^2 t_0^2}
629 * \f]
630 *
631 * If you set \f$ T_0 \le 0 \f$ and \f$ t_0 = 0\f$ this reverts to a simple \f$ 1/f^\alpha \f$ law (i.e.
632 * it treats this as infinite outer scale and inner scale).
633 *
634 * \returns error_t::noerror on success
635 *
636 * \tparam floatT a floating point
637 */
638template <typename floatT,
639 typename floatfT,
640 typename alphaT,
641 typename T0T = double,
642 typename t0T = double,
643 typename betaT = double>
644mx::error_t vonKarmanPSD( std::vector<floatT> &psd, ///< [out] the PSD vector, will be resized.
645 std::vector<floatfT> &f, ///< [in] the frequency vector
646 alphaT alpha, ///< [in] the exponent, by convention @f$ alpha > 0 @f$.
647 T0T T0 = 0, ///< [in] the outer scale, default is 0 (not used).
648 t0T t0 = 0, ///< [in] the inner scale, default is 0 (not used).
649 betaT beta = 1 ///< [in] the scaling constant, default is 1
650)
651{
652
653 floatT T02;
654 if( T0 > 0 )
655 {
656 T02 = 1.0 / ( T0 * T0 );
657 }
658 else
659 {
660 T02 = 0;
661 }
662
663 floatT sqrt_alpha = 0.5 * alpha;
664
665 floatT _beta;
666 if( beta <= 0 )
667 {
668 _beta = 1;
669 }
670 else
671 {
672 _beta = beta;
673 }
674
675 psd.resize( f.size() );
676
677 if( t0 > 0 )
678 {
679 floatT _t0 = static_cast<floatT>( t0 );
680 for( size_t i = 0; i < f.size(); ++i )
681 {
682 psd[i] = _beta / pow( pow( f[i], 2 ) + T02, sqrt_alpha ) * exp( -1 * pow( f[i] * _t0, 2 ) );
683 }
684 }
685 else
686 {
687 for( size_t i = 0; i < f.size(); ++i )
688 {
689 psd[i] = _beta / pow( pow( f[i], 2 ) + T02, sqrt_alpha );
690 }
691 }
692
694}
695
696/// Generate a 1-D "knee" PSD
697/**
698 * Populates an Eigen array with
699 *
700 * \f[
701 * P(f) = \frac{\beta}{ 1 + (f/f_n)^{\alpha}}
702 * \f]
703 *
704 * If you set \f$ T_0 \le 0 \f$ and \f$ t_0 = 0\f$ this reverts to a simple \f$ 1/f^\alpha \f$ law (i.e.
705 * it treats this as infinite outer scale and inner scale).
706 *
707 * \tparam floatT a floating point
708 */
709template <typename floatT>
710int kneePSD( std::vector<floatT> &psd, ///< [out] the PSD vector, will be resized.
711 std::vector<floatT> &f, ///< [in] the frequency vector
712 floatT beta, ///< [in] the scaling constant
713 floatT fn, ///< [in] the knee frequency
714 floatT alpha ///< [in] the exponent, by convention @f$ alpha > 0 @f$.
715)
716{
717
718 psd.resize( f.size() );
719
720 for( int i = 0; i < f.size(); ++i )
721 {
722 floatT p = beta / ( 1 + pow( f[i] / fn, alpha ) );
723 psd[i] = p;
724 }
725
726 return 0;
727}
728
729/// Generates a von Karman power spectrum
730/**
731 * Populates an Eigen array with
732 *
733 * \f[
734 * P(k) = \frac{\beta}{ (k^2 + (1/L_0)^2)^{\alpha/2}} e^{ - k^2 l_0^2}
735 * \f]
736 *
737 * If you set \f$ L_0 \le 0 \f$ and \f$ l_0 = 0\f$ this reverts to a simple \f$ 1/f^\alpha \f$ law (i.e.
738 * it treats this as infinite outer scale and inner scale).
739 *
740 * \param [out] psd is the array to populate, allocated.
741 * \param [in] freq is a frequency grid, must be the same logical size as psd
742 * \param [in] alpha is the power law exponent, by convention @f$ alpha > 0 @f$.
743 * \param [in] L0 [optional] is the outer scale.
744 * \param [in] l0 [optional] is the inner scale.
745 * \param [in] beta [optional] is a normalization constant to multiply the raw spectrum by. If beta==-1 (default)
746 * then the PSD is normalized using \ref oneoverf_norm.
747 *
748 * \tparam eigenArrp is the Eigen array type of the psd
749 * \tparam eigenArrf is the Eigen array type of the frequency grid
750 */
751template <typename eigenArrp, typename eigenArrf, typename alphaT, typename L0T, typename l0T, typename betaT>
752void vonKarmanPSD( eigenArrp &psd, eigenArrf &freq, alphaT alpha, L0T L0 = 0, l0T l0 = 0, betaT beta = -1 )
753{
754 typedef typename eigenArrp::Scalar Scalar;
755
756 typename eigenArrp::Index dim_1, dim_2;
757 Scalar p;
758
759 dim_1 = psd.rows();
760 dim_2 = psd.cols();
761
762 Scalar _beta;
763
764 if( beta == -1 )
765 {
766 Scalar fmin;
767 Scalar fmax;
768
769 fmax = freq.abs().maxCoeff();
770
771 // Find minimum non-zero Coeff.
772 fmin = ( freq.abs() > 0 ).select( freq.abs(), freq.abs() + fmax ).minCoeff();
773
774 _beta = beta = oneoverf_norm( fmin, fmax, static_cast<Scalar>( alpha ) );
775 }
776 else
777 _beta = static_cast<Scalar>( beta );
778
779 Scalar L02;
780 if( L0 > 0 )
781 L02 = 1.0 / ( L0 * L0 );
782 else
783 L02 = 0;
784
785 Scalar sqrt_alpha = 0.5 * alpha; // std::sqrt(alpha);
786
787 for( int ii = 0; ii < dim_1; ++ii )
788 {
789 for( int jj = 0; jj < dim_2; ++jj )
790 {
791 if( freq( ii, jj ) == 0 && L02 == 0 )
792 {
793 p = 0;
794 }
795 else
796 {
797 p = _beta / pow( pow( freq( ii, jj ), 2 ) + L02, sqrt_alpha );
798 if( l0 > 0 )
799 p *= exp( -1 * pow( freq( ii, jj ) * static_cast<Scalar>( l0 ), 2 ) );
800 }
801 psd( ii, jj ) = p;
802 }
803 }
804}
805
806/// Augment a 1-sided PSD to standard 2-sided FFT form.
807/** Allocates psdTwoSided to hold a flipped copy of psdOneSided.
808 * Default assumes that psdOneSided[0] corresponds to 0 frequency,
809 * but this can be changed by setting zeroFreq to a non-zero value.
810 * In this case psdTwoSided[0] is set to 0, and the augmented psd
811 * is shifted by 1.
812 *
813 * To illustrate, the bins are re-ordered as:
814 * \verbatim
815 * {1,2,3,4,5} --> {0,1,2,3,4,5,-4,-3,-2,-1}
816 * \endverbatim
817 *
818 * The output is scaled so that the total power remains the same. The 0-freq and
819 * Nyquist freq are not scaled.
820 *
821 *
822 * Entries in psdOneSided are cast to the value_type of psdTwoSided,
823 * for instance to allow for conversion to complex type.
824 *
825 */
826template <typename vectorTout, typename vectorTin>
828 vectorTout &psdTwoSided, ///< [out] on return contains the FFT storage order copy of psdOneSided.
829 vectorTin &psdOneSided, ///< [in] the one-sided PSD to augment
830 bool addZeroFreq =
831 false, ///< [in] [optional] set to true if psdOneSided does not contain a zero frequency component.
832 typename vectorTin::value_type scale = 0.5 ///< [in] [optional] value to scale the input by when copying to the
833 ///< output. The default 0.5 re-normalizes for a 2-sided PSD.
834)
835{
836 typedef typename vectorTout::value_type outT;
837
838 bool needZero = 1;
839
840 size_t N;
841
842 if( addZeroFreq == 0 )
843 {
844 needZero = 0;
845 N = 2 * psdOneSided.size() - 2;
846 }
847 else
848 {
849 N = 2 * psdOneSided.size();
850 }
851
852 psdTwoSided.resize( N );
853
854 // First set the 0-freq point
855 if( needZero )
856 {
857 psdTwoSided[0] = outT( 0.0 );
858 }
859 else
860 {
861 psdTwoSided[0] = outT( psdOneSided[0] );
862 }
863
864 // Now set all the rest.
865 unsigned long i;
866 for( i = 0; i < psdOneSided.size() - 1 - ( 1 - needZero ); ++i )
867 {
868 psdTwoSided[i + 1] = outT( psdOneSided[i + ( 1 - needZero )] * scale );
869 psdTwoSided[i + psdOneSided.size() + needZero] = outT( psdOneSided[psdOneSided.size() - 2 - i] * scale );
870 }
871 psdTwoSided[i + 1] = outT( psdOneSided[i + ( 1 - needZero )] );
872}
873
874/// Augment a 1-sided frequency scale to standard FFT form.
875/** Allocates freqTwoSided to hold a flipped copy of freqOneSided.
876 * If freqOneSided[0] is not 0, freqTwoSided[0] is set to 0, and the augmented
877 * frequency scale is shifted by 1.
878 *
879 * Example:
880 *
881 * {1,2,3,4,5} --> {0,1,2,3,4,5,-4,-3,-2,-1}
882 *
883 */
884template <typename T>
886 std::vector<T> &freqTwoSided, ///< [out] on return contains the FFT storage order copy of freqOneSided.
887 std::vector<T> &freqOneSided ///< [in] the one-sided frequency scale to augment
888)
889{
890 int needZero = 1;
891
892 size_t N;
893
894 if( freqOneSided[0] != 0 )
895 {
896 N = 2 * freqOneSided.size();
897 }
898 else
899 {
900 needZero = 0;
901 N = 2 * freqOneSided.size() - 2;
902 }
903
904 freqTwoSided.resize( N );
905
906 if( needZero )
907 {
908 freqTwoSided[0] = 0.0;
909 }
910 else
911 {
912 freqTwoSided[0] = freqOneSided[0]; // 0
913 }
914
915 int i;
916 for( i = 0; i < freqOneSided.size() - 1 - ( 1 - needZero ); ++i )
917 {
918 freqTwoSided[i + 1] = freqOneSided[i + ( 1 - needZero )];
919 freqTwoSided[i + freqOneSided.size() + needZero] = -freqOneSided[freqOneSided.size() - 2 - i];
920 }
921 freqTwoSided[i + 1] = freqOneSided[i + ( 1 - needZero )];
922}
923
924/// Rebin a PSD, including its frequency scale, to a larger frequency bin size (fewer bins)
925/** The rebinning uses trapezoid integration within bins to ensure minimum signal loss.
926 *
927 * Maintains DFT sampling. That is, if initial frequency grid is 0,0.1,0.2...
928 * and the binSize is 1.0, the new grid will be 0,1,2 (as opposed to 0.5, 1.5, 2.5).
929 *
930 * This introduces a question of what to do with first half-bin, which includes 0. It can be
931 * integrated (binAtZero = true, the default). This may cause inaccurate behavior if the value of the PSD when
932 * f=0 is important (e.g. when analyzing correlated noise), so setting binAtZero=false causes the f=0 value to be
933 * copied (using the nearest neighbor if no f=0 point is in the input.
934 *
935 * The last half bin is always integrated.
936 *
937 * The output is variance normalized to match the input variance.
938 *
939 * \tparam realT the real floating point type
940 */
941template <typename realT>
942int rebin1SidedPSD( std::vector<realT> &binFreq, ///< [out] the binned frequency scale, resized.
943 std::vector<realT> &binPSD, ///< [out] the binned PSD, resized.
944 std::vector<realT> &freq, ///< [in] the frequency scale of the PSD to bin.
945 std::vector<realT> &PSD, ///< [in] the PSD to bin.
946 realT binSize, ///< [in] in same units as freq
947 bool binAtZero = true ///< [in] [optional] controls whether the zero point is binned or copied.
948)
949{
950 binFreq.clear();
951 binPSD.clear();
952
953 realT sumPSD = 0;
954 realT startFreq = 0;
955 realT sumFreq = 0;
956 int nSum = 0;
957
958 int i = 0;
959
960 realT df = freq[1] - freq[0];
961
962 // Now move to first bin
963 while( freq[i] <= 0.5 * binSize + 0.5 * df )
964 {
965 sumPSD += PSD[i];
966 ++nSum;
967 ++i;
968 if( i >= freq.size() )
969 break;
970 }
971
972 if( !binAtZero )
973 {
974 binFreq.push_back( 0 );
975 binPSD.push_back( PSD[0] );
976 }
977 else
978 {
979 binFreq.push_back( 0 );
980 binPSD.push_back( sumPSD / nSum );
981 }
982
983 --i;
984 startFreq = freq[i];
985 nSum = 0;
986 sumFreq = 0;
987 sumPSD = 0;
988
989 while( i < freq.size() )
990 {
991 realT sc = 0.5; // First one is multiplied by 1/2 for trapezoid rule.
992 while( freq[i] - startFreq + 0.5 * df < binSize )
993 {
994 sumFreq += freq[i];
995 sumPSD += sc * PSD[i];
996 sc = 1.0;
997 ++nSum;
998
999 ++i;
1000 if( i >= freq.size() - 1 )
1001 break; // break 1 element early so last point is mult by 0.5
1002 }
1003
1004 if( i < freq.size() )
1005 {
1006 sumFreq += freq[i];
1007 sumPSD += 0.5 * PSD[i]; // last one is multiplied by 1/2 for trapezoid rule
1008 ++nSum;
1009 ++i;
1010 }
1011
1012 // Check if this is last point
1013 if( i < freq.size() )
1014 {
1015 binFreq.push_back( sumFreq / nSum );
1016 }
1017 else
1018 {
1019 // last point frequencyis not averaged.
1020 binFreq.push_back( freq[freq.size() - 1] );
1021 }
1022
1023 binPSD.push_back( sumPSD / ( nSum - 1 ) );
1024
1025 sumFreq = 0;
1026 sumPSD = 0;
1027 nSum = 0;
1028 if( i >= freq.size() )
1029 break;
1030
1031 --i; // Step back one, so averages are edge to edge.
1032
1033 startFreq = freq[i];
1034 }
1035
1036 // Now normalize variance
1037 realT var = psdVar( freq, PSD );
1038 realT binv = psdVar( binFreq, binPSD );
1039
1040 for( int i = 0; i < binFreq.size(); ++i )
1041 binPSD[i] *= var / binv;
1042
1043 return 0;
1044}
1045///@}
1046
1047} // namespace sigproc
1048} // namespace mx
1049
1050#endif // psdUtils_hpp
The Fast Fourier Transform interface.
error_t
The mxlib error codes.
Definition error_t.hpp:26
@ noerror
No error has occurred.
Definition error_t.hpp:27
@ invalidarg
An argument was invalid.
Definition error_t.hpp:29
error_t mxlib_error_report(const error_t &code, const std::string &expl, const std::source_location &loc=std::source_location::current())
Print a report to stderr given an mxlib error_t code and explanation and return the code.
Definition error.hpp:331
void oneoverf_psd(eigenArrp &psd, eigenArrf &freq, typename eigenArrp::Scalar alpha, typename eigenArrp::Scalar beta=-1)
Generates a power spectrum.
Definition psdUtils.hpp:580
realT oneoverf_norm(realT fmin, realT fmax, realT alpha)
Calculate the normalization for a 1-D PSD.
Definition psdUtils.hpp:414
void augment1SidedPSD(vectorTout &psdTwoSided, vectorTin &psdOneSided, bool addZeroFreq=false, typename vectorTin::value_type scale=0.5)
Augment a 1-sided PSD to standard 2-sided FFT form.
Definition psdUtils.hpp:827
eigenArrT::Scalar psdVarDisabled(eigenArrT &freq, eigenArrT &PSD, bool trap=true)
Calculate the variance of a PSD.
Definition psdUtils.hpp:161
realT oneoverk_norm(realT kmin, realT kmax, realT alpha)
Calculate the normalization for a 2-D PSD.
Definition psdUtils.hpp:432
realT psdVar1sided(realT df, const realT *PSD, size_t sz, realT half=0.5)
Calculate the variance of a 1-D, 1-sided PSD.
Definition psdUtils.hpp:65
void augment1SidedPSDFreq(std::vector< T > &freqTwoSided, std::vector< T > &freqOneSided)
Augment a 1-sided frequency scale to standard FFT form.
Definition psdUtils.hpp:885
realT psdVar(const std::vector< realT > &f, const std::vector< realT > &PSD, realT half=0.5)
Calculate the variance of a 1-D PSD.
Definition psdUtils.hpp:135
realT psdVar2sided(realT df, const realT *PSD, size_t sz, realT half=0.5)
Calculate the variance of a 1-D, 2-sided PSD.
Definition psdUtils.hpp:99
mx::error_t vonKarmanPSD(std::vector< floatT > &psd, std::vector< floatfT > &f, alphaT alpha, T0T T0=0, t0T t0=0, betaT beta=1)
Generate a 1-D von Karman power spectrum.
Definition psdUtils.hpp:644
realT freq_sampling(size_t dim, realT f_max)
Calculates the frequency sampling for a grid given maximum dimension and maximum frequency.
Definition psdUtils.hpp:199
int kneePSD(std::vector< floatT > &psd, std::vector< floatT > &f, floatT beta, floatT fn, floatT alpha)
Generate a 1-D "knee" PSD.
Definition psdUtils.hpp:710
int normPSD(std::vector< floatT > &psd, std::vector< floatT > &f, floatParamT normT, floatT fmin=std::numeric_limits< floatT >::min(), floatT fmax=std::numeric_limits< floatT >::max())
Normalize a 1-D PSD to have a given variance.
Definition psdUtils.hpp:448
int frequencyGrid(std::vector< realT > &vec, realParamT dt, bool fftOrder=true)
Create a 1-D frequency grid.
Definition psdUtils.hpp:258
int rebin1SidedPSD(std::vector< realT > &binFreq, std::vector< realT > &binPSD, std::vector< realT > &freq, std::vector< realT > &PSD, realT binSize, bool binAtZero=true)
Rebin a PSD, including its frequency scale, to a larger frequency bin size (fewer bins).
Definition psdUtils.hpp:942
Declarations of some libarary wide utilities.
The mxlib c++ namespace.
Definition mxlib.hpp:37
Header for the std::vector utilities.