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