8#ifndef modalPsdProcessor_hpp
9#define modalPsdProcessor_hpp
18#include <mx/error/error.hpp>
19#include <mx/math/func/moffat.hpp>
20#include <mx/math/vectorUtils.hpp>
29template <
typename realT>
406 const std::vector<realT> &measuredPsd,
407 const std::vector<realT> &freq,
410 realT lpContinuumFreq =
static_cast<realT
>( 0 ),
412 const std::vector<realT> *etfPsd =
nullptr,
413 const std::vector<realT> *ntfPsd =
nullptr
418 std::vector<realT> &noisePsd,
420 const std::vector<realT> &measuredPsd,
421 const std::vector<realT> &freq,
432 const std::vector<realT> &processPsd,
433 const std::vector<realT> &freq,
435 realT continuumWidthHz
463 realT requestedNormFreq
470 const std::vector<realT> &rawProcessPsd,
471 const std::vector<realT> &freq,
478 realT &crossoverFreq,
479 const std::vector<realT> &smoothedProcessPsd,
480 const std::vector<realT> &noisePsd,
481 const std::vector<realT> &freq,
482 realT maxFreqFraction
489 realT &powerLawMatchFreq,
490 realT &powerLawOnlyAboveFreq,
491 const std::vector<realT> &rawProcessPsd,
492 const std::vector<realT> &smoothedProcessPsd,
493 const std::vector<realT> &noisePsd,
494 const std::vector<realT> &freq,
495 std::string powerLawCrossoverMode,
496 realT powerLawAutoMaxFreqFraction
502 const std::vector<realT> &freq,
505 realT powerLawNormFreq
510 const std::vector<realT> &freq,
518 const std::vector<realT> &anchorProcessPsd,
519 const std::vector<realT> &freq,
521 realT powerLawNormFreq,
522 realT powerLawMatchFreq,
523 realT powerLawMatchFallbackWindowHz
560 realT &powerLawIndex,
562 const std::vector<realT> &rawProcessPsd,
563 const std::vector<realT> &freq,
567 realT includeMatchFreqHz =
static_cast<realT
>( 0 )
573 std::vector<realT> &continuumPsd,
574 realT &extrapolation,
576 const std::vector<realT> &rawProcessPsd,
577 const std::vector<realT> &anchorProcessPsd,
578 const std::vector<realT> &noisePsd,
579 const std::vector<realT> &freq,
581 realT powerLawNormFreq,
582 realT powerLawMatchFreq,
583 realT powerLawMatchFallbackWindowHz,
585 bool fitPowerLawIndex =
false,
591 realT *usedPowerLawIndex =
nullptr,
592 size_t *fitBinsUsed =
nullptr
598 const std::vector<realT> &freq,
600 realT powerLawNormFreq
607 const std::vector<realT> &rawProcessPsd,
608 const std::vector<realT> &continuumPsd,
617 const std::vector<realT> &rawProcessPsd,
618 const std::vector<realT> &continuumPsd,
619 const std::vector<realT> &freq,
620 realT peakDetectWidthHz,
621 realT peakDetectFactor,
623 realT peakDetectBroadFactor,
625 realT peakDetectMinWidthLog
633 const std::vector<realT> &sourcePsd,
634 const std::vector<realT> &continuumPsd,
635 const std::vector<realT> &freq,
643 const std::vector<realT> &rawProcessPsd,
644 const std::vector<realT> &continuumPsd,
645 const std::vector<realT> &freq,
646 realT peakDetectWidthHz,
647 realT peakDetectFactor,
648 realT peakDetectBroadFactor,
649 realT peakDetectMinWidthLog,
650 int peakDetectPasses,
657 std::vector<identifiedPeak1D> &peaks,
658 std::vector<unsigned char> &repairMask,
659 const std::vector<realT> &rawProcessPsd,
660 const std::vector<realT> &noisePsd,
661 const std::vector<realT> &continuumPsd,
662 const std::vector<realT> &freq,
670 std::vector<unsigned char> &repairMask,
671 const std::vector<realT> &rawProcessPsd,
672 const std::vector<realT> &noisePsd,
673 const std::vector<realT> &continuumPsd,
674 const std::vector<realT> &freq,
682 realT &extrapolation,
685 std::vector<unsigned char> &repairMask,
686 const std::vector<realT> &measuredPsd,
687 const std::vector<realT> &anchorProcessPsd,
689 const std::vector<realT> &noisePsd,
690 const std::vector<realT> &freq,
692 realT *usedPowerLawIndex =
nullptr,
693 size_t *fitBinsUsed =
nullptr
700 realT &extrapolation,
701 std::vector<identifiedPeak1D> &peaks,
702 std::vector<unsigned char> &repairMask,
703 const std::vector<realT> &measuredPsd,
704 const std::vector<realT> &anchorProcessPsd,
706 const std::vector<realT> &noisePsd,
707 const std::vector<realT> &freq,
715 realT &extrapolation,
716 const std::vector<realT> &measuredPsd,
717 const std::vector<realT> &noisePsd,
718 const std::vector<realT> &freq,
719 realT powerLawNormFreq,
720 realT powerLawMatchFreq,
721 realT powerLawMatchFallbackWindowHz
728 const std::vector<realT> &freq,
729 const std::vector<unsigned char> &repairMask,
738template <
typename realT>
740 const std::vector<realT> &measuredPsd,
741 const std::vector<realT> &freq,
744 realT lpContinuumFreq,
745 realT lpContinuumWidthHz,
746 const std::vector<realT> *etfPsd,
747 const std::vector<realT> *ntfPsd )
749 if( measuredPsd.size() != freq.size() )
751 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr,
752 "Measured PSD and frequency grid must be the same size" );
755 if( measuredPsd.size() < 2 )
757 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr,
"At least two frequency bins are required" );
796 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
797 "Dropout gap factor must be between 0 and 1" );
802 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
803 "Dropout tiny factor must be between 0 and 1" );
808 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
809 "Power-law-only-above frequency must be non-negative" );
814 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
815 "Power-law match fallback window must be non-negative" );
820 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
"Power-law blend bins must be non-negative" );
825 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
"Dropout max bins must be at least one" );
832 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
833 "Invalid power-law exponent fit range or bin width" );
838 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
844 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
850 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
851 "Unknown noise-estimation statistic: " +
857 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
858 "Low-frequency noise-estimate maximum must be non-negative" );
863 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
864 "Unknown closed-loop OL estimate method: " +
870 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
871 "Unknown power-law crossover mode: " +
878 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
879 "Automatic power-law crossover smoothing width must be positive" );
885 return mx::error_report<mx::verbose::d>(
886 mx::error_t::invalidarg,
887 "Automatic power-law crossover maximum-frequency fraction must be between 0 and 1" );
890 const std::vector<realT> &noiseEstimatePsd = measuredPsd;
892 mx::error_t errc = estimateNoisePsd( result.
m_noisePsd,
905 std::vector<realT> processMeasuredPsd = measuredPsd;
906 std::vector<realT> processNoisePsd = result.
m_noisePsd;
909 if( etfPsd ==
nullptr )
911 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
912 "Closed-loop noise estimation requires an ETF PSD" );
915 if( etfPsd->size() != measuredPsd.size() )
917 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr,
"ETF PSD must match the measured PSD size" );
922 if( ntfPsd ==
nullptr )
924 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
925 "NTF-aware closed-loop noise estimation requires an NTF PSD" );
928 if( ntfPsd->size() != measuredPsd.size() )
930 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr,
931 "NTF PSD must match the measured PSD size" );
935 const realT tiny = std::numeric_limits<realT>::min();
936 for(
size_t n = 0; n < measuredPsd.size(); ++n )
938 const realT useEtf = std::max( ( *etfPsd )[n], tiny );
942 closedLoopNoise = result.
m_noisePsd[n] * std::max( ( *ntfPsd )[n], tiny );
945 const realT rawClosedLoop = std::max( measuredPsd[n] - closedLoopNoise, tiny );
946 const realT openLoopProcess = rawClosedLoop / useEtf;
951 processNoisePsd[n] = std::max( closedLoopNoise, tiny );
952 processMeasuredPsd[n] = openLoopProcess + processNoisePsd[n];
966 const realT tiny = std::numeric_limits<realT>::min();
968 for(
size_t n = 0; n < processMeasuredPsd.size(); ++n )
970 result.
m_rawProcessPsd[n] = std::max( processMeasuredPsd[n] - processNoisePsd[n], tiny );
973 if( effectiveConfig.
m_method ==
"power-law-only" || effectiveConfig.
m_method ==
"moffat-peaks" )
987 for(
size_t n = 0; n < processMeasuredPsd.size(); ++n )
989 processMeasuredPsd[n] = result.
m_rawProcessPsd[n] + processNoisePsd[n];
1026 std::vector<unsigned char> processRepairMask;
1028 if( effectiveConfig.
m_method ==
"legacy" )
1046 else if( effectiveConfig.
m_method ==
"power-law-only" )
1049 size_t fitBinsUsed = 0;
1050 errc = estimateProcessPsdPowerLawOnly( result.
m_processPsd,
1072 else if( effectiveConfig.
m_method ==
"moffat-peaks" )
1074 size_t anchorIndex = 0;
1075 errc = estimateProcessPsdMoffatPeaks( result.
m_processPsd,
1089 std::vector<realT> rawProcessPsd( processMeasuredPsd.size() );
1090 const realT tiny = std::numeric_limits<realT>::min();
1091 for(
size_t n = 0; n < processMeasuredPsd.size(); ++n )
1093 rawProcessPsd[n] = std::max( processMeasuredPsd[n] - processNoisePsd[n], tiny );
1096 std::vector<realT> continuumPsd;
1098 size_t fitBinsUsed = 0;
1099 errc = estimatePowerLawContinuum( continuumPsd,
1131 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
1132 "Unknown process method: " + effectiveConfig.
m_method );
1135 if( effectiveConfig.
m_method ==
"power-law-only" || effectiveConfig.
m_method ==
"moffat-peaks" )
1151 const realT tiny = std::numeric_limits<realT>::min();
1152 std::vector<realT> rawProcessPsd( processMeasuredPsd.size() );
1153 for(
size_t n = 0; n < processMeasuredPsd.size(); ++n )
1155 rawProcessPsd[n] = std::max( processMeasuredPsd[n] - processNoisePsd[n], tiny );
1158 std::vector<realT> continuumPsd;
1159 size_t rematchAnchorIndex = 0;
1160 errc = estimatePowerLawContinuum( continuumPsd,
1169 static_cast<realT
>( 0 ),
1184 "auto-smoothed-crossing"
1185 ?
static_cast<realT
>( 0 )
1192 errc = buildPowerLawContinuum( continuumPsd,
1205 :
static_cast<realT
>( 0 );
1207 if( effectiveConfig.
m_method ==
"power-law-only" )
1209 errc = buildPowerLawOnlyProcessFromContinuum( result.
m_processPsd,
1220 errc = buildMoffatProcessFromContinuum( result.
m_processPsd,
1255 return mx::error_t::noerror;
1258template <
typename realT>
1261 const std::vector<realT> &measuredPsd,
1262 const std::vector<realT> &freq,
1264 std::string noiseEstimateRange,
1265 std::string noiseEstimateStatistic,
1266 realT noiseEstimateLowFreqMaxHz )
1268 if( measuredPsd.size() < 2 || measuredPsd.size() != freq.size() )
1270 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr,
1271 "PSD and frequency grid must match and have at least two bins" );
1274 noiseEstimateRange = normalizeNoiseEstimateRange( noiseEstimateRange );
1275 noiseEstimateStatistic = normalizeNoiseEstimateStatistic( noiseEstimateStatistic );
1276 size_t f0 = measuredPsd.size() / 2;
1277 size_t f1 = measuredPsd.size();
1278 if( noiseEstimateRange ==
"low-freq" )
1280 f0 = measuredPsd.size() > 1 ? 1 : 0;
1281 f1 = std::max( f0 +
static_cast<size_t>( 1 ), measuredPsd.size() / 2 );
1282 if( noiseEstimateLowFreqMaxHz >
static_cast<realT
>( 0 ) )
1284 size_t cappedF1 = f0;
1285 while( cappedF1 < measuredPsd.size() && freq[cappedF1] <= noiseEstimateLowFreqMaxHz )
1290 f1 = std::max( f0 +
static_cast<size_t>( 1 ), std::min( f1, cappedF1 ) );
1293 else if( noiseEstimateRange !=
"high-freq" )
1295 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
1296 "Unknown noise-estimation range: " + noiseEstimateRange );
1301 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr,
"PSD is too short for noise fitting" );
1304 std::vector<realT> npsd( f1 - f0 );
1305 const realT tiny = std::numeric_limits<realT>::min();
1306 for(
size_t f = f0; f < f1; ++f )
1308 npsd[f - f0] = log10( std::max( measuredPsd[f], tiny ) );
1311 if( noiseEstimateStatistic ==
"minimum" )
1313 auto minIt = std::min_element( npsd.begin(), npsd.end() );
1314 noiseFloor = pow(
static_cast<realT
>( 10 ), *minIt );
1315 noisePsd.assign( measuredPsd.size(), noiseFloor );
1316 return mx::error_t::noerror;
1319 if( noiseEstimateStatistic !=
"percentile" )
1321 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
1322 "Unknown noise-estimation statistic: " + noiseEstimateStatistic );
1325 realT pct =
static_cast<realT
>( 0.25 );
1328 pct =
static_cast<realT
>( 0.05 );
1331 size_t nthIndex =
static_cast<size_t>( pct *
static_cast<realT
>( npsd.size() ) );
1332 if( nthIndex >= npsd.size() )
1334 nthIndex = npsd.size() - 1;
1337 auto nth = npsd.begin() + nthIndex;
1338 std::nth_element( npsd.begin(), nth, npsd.end() );
1340 noiseFloor = pow(
static_cast<realT
>( 10 ), *nth );
1341 noisePsd.assign( measuredPsd.size(), noiseFloor );
1343 return mx::error_t::noerror;
1346template <
typename realT>
1349 std::transform( domain.begin(),
1352 [](
unsigned char c )
1356 return static_cast<char>(
'-' );
1359 return static_cast<char>( std::tolower( c ) );
1365template <
typename realT>
1368 std::transform( range.begin(),
1371 [](
unsigned char c )
1375 return static_cast<char>(
'-' );
1378 return static_cast<char>( std::tolower( c ) );
1384template <
typename realT>
1387 std::transform( statistic.begin(),
1390 [](
unsigned char c )
1394 return static_cast<char>(
'-' );
1397 return static_cast<char>( std::tolower( c ) );
1403template <
typename realT>
1406 std::transform( method.begin(),
1409 [](
unsigned char c )
1413 return static_cast<char>(
'-' );
1416 return static_cast<char>( std::tolower( c ) );
1422template <
typename realT>
1425 std::transform( mode.begin(),
1428 [](
unsigned char c )
1432 return static_cast<char>(
'-' );
1435 return static_cast<char>( std::tolower( c ) );
1438 if( mode ==
"auto" || mode ==
"automatic" || mode ==
"auto-crossing" )
1440 return "auto-smoothed-crossing";
1446template <
typename realT>
1449 for(
size_t n = 0; n < freq.size(); ++n )
1451 if( freq[n] >
static_cast<realT
>( 0 ) )
1460template <
typename realT>
1462 const std::vector<realT> &rawProcessPsd,
1463 const std::vector<realT> &freq,
1464 realT smoothWidthHz )
1466 if( rawProcessPsd.size() != freq.size() )
1468 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr,
1469 "Smoothed disturbance PSD inputs must have "
1470 "matching PSD and frequency sizes" );
1473 if( rawProcessPsd.size() < 3 )
1475 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr,
1476 "Smoothed disturbance PSD requires at least three bins" );
1479 if( smoothWidthHz <=
static_cast<realT
>( 0 ) )
1481 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
1482 "Smoothed disturbance PSD width must be positive" );
1485 const size_t firstPositive = firstPositiveFreqIndex( freq );
1486 if( firstPositive >= freq.size() || freq[firstPositive] <=
static_cast<realT
>( 0 ) )
1488 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
1489 "Automatic power-law crossover requires positive frequencies" );
1492 if( firstPositive + 1 >= freq.size() )
1494 smoothedProcessPsd = rawProcessPsd;
1495 return mx::error_t::noerror;
1498 const realT df = freq[firstPositive + 1] - freq[firstPositive];
1499 if( df <=
static_cast<realT
>( 0 ) )
1501 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
1502 "Automatic power-law crossover requires increasing frequency bins" );
1505 int win = std::max( 3,
static_cast<int>( std::lround( smoothWidthHz / df ) ) );
1511 const realT tiny = std::numeric_limits<realT>::min();
1512 std::vector<realT> logRaw( rawProcessPsd.size() );
1513 for(
size_t n = 0; n < rawProcessPsd.size(); ++n )
1515 logRaw[n] = log10( std::max( rawProcessPsd[n], tiny ) );
1518 std::vector<realT> logSmooth;
1519 mx::math::vectorSmoothMedian( logSmooth, logRaw, win );
1520 smoothedProcessPsd.resize( rawProcessPsd.size() );
1521 for(
size_t n = 0; n < rawProcessPsd.size(); ++n )
1523 smoothedProcessPsd[n] = pow(
static_cast<realT
>( 10 ), logSmooth[n] );
1526 return mx::error_t::noerror;
1529template <
typename realT>
1531 const std::vector<realT> &smoothedProcessPsd,
1532 const std::vector<realT> &noisePsd,
1533 const std::vector<realT> &freq,
1534 realT maxFreqFraction )
1536 if( smoothedProcessPsd.size() != noisePsd.size() || smoothedProcessPsd.size() != freq.size() )
1538 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr,
1539 "Automatic power-law crossover inputs must have "
1540 "matching PSD and frequency sizes" );
1543 if( smoothedProcessPsd.size() < 3 )
1545 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr,
1546 "Automatic power-law crossover requires at least three bins" );
1549 const size_t firstPositive = firstPositiveFreqIndex( freq );
1550 if( firstPositive >= freq.size() || freq[firstPositive] <=
static_cast<realT
>( 0 ) )
1552 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
1553 "Automatic power-law crossover requires positive frequencies" );
1556 size_t lastSearch = freq.size() - 1;
1557 if( maxFreqFraction >
static_cast<realT
>( 0 ) && maxFreqFraction <
static_cast<realT
>( 1 ) )
1559 realT maxSearchFreq = maxFreqFraction * freq.back();
1561 std::upper_bound( freq.begin() +
static_cast<std::ptrdiff_t
>( firstPositive ), freq.end(), maxSearchFreq );
1562 if( upper == freq.begin() +
static_cast<std::ptrdiff_t
>( firstPositive ) )
1564 lastSearch = firstPositive;
1568 lastSearch =
static_cast<size_t>( ( upper - freq.begin() ) - 1 );
1572 if( lastSearch <= firstPositive )
1574 crossoverFreq = freq[firstPositive];
1575 return mx::error_t::noerror;
1578 bool foundCrossing =
false;
1579 realT lastCrossingFreq = freq[firstPositive];
1580 for(
size_t n = firstPositive; n < lastSearch; ++n )
1582 realT d0 = smoothedProcessPsd[n] - noisePsd[n];
1583 realT d1 = smoothedProcessPsd[n + 1] - noisePsd[n + 1];
1585 if( d0 ==
static_cast<realT
>( 0 ) && d1 ==
static_cast<realT
>( 0 ) )
1587 lastCrossingFreq = freq[n + 1];
1588 foundCrossing =
true;
1592 if( ( d0 <=
static_cast<realT
>( 0 ) && d1 >=
static_cast<realT
>( 0 ) ) ||
1593 ( d0 >=
static_cast<realT
>( 0 ) && d1 <=
static_cast<realT
>( 0 ) ) )
1595 realT alpha =
static_cast<realT
>( 0 );
1598 alpha = d0 / ( d0 - d1 );
1601 alpha = std::max(
static_cast<realT
>( 0 ), std::min(
static_cast<realT
>( 1 ), alpha ) );
1602 lastCrossingFreq = freq[n] + alpha * ( freq[n + 1] - freq[n] );
1603 foundCrossing =
true;
1609 crossoverFreq = lastCrossingFreq;
1610 return mx::error_t::noerror;
1613 size_t minimumIndex = firstPositive;
1614 realT minimumPsd = smoothedProcessPsd[firstPositive];
1615 for(
size_t n = firstPositive + 1; n <= lastSearch; ++n )
1617 if( smoothedProcessPsd[n] <= minimumPsd )
1619 minimumPsd = smoothedProcessPsd[n];
1624 crossoverFreq = freq[minimumIndex];
1625 return mx::error_t::noerror;
1628template <
typename realT>
1630 realT &powerLawOnlyAboveFreq,
1631 const std::vector<realT> &rawProcessPsd,
1632 const std::vector<realT> &smoothedProcessPsd,
1633 const std::vector<realT> &noisePsd,
1634 const std::vector<realT> &freq,
1635 std::string powerLawCrossoverMode,
1636 realT powerLawAutoMaxFreqFraction )
1638 powerLawCrossoverMode = normalizePowerLawCrossoverMode( powerLawCrossoverMode );
1639 if( powerLawCrossoverMode !=
"auto-smoothed-crossing" )
1641 return mx::error_t::noerror;
1644 static_cast<void>( rawProcessPsd );
1646 realT crossoverFreq =
static_cast<realT
>( 0 );
1648 findAutoPowerLawCrossoverFreq( crossoverFreq, smoothedProcessPsd, noisePsd, freq, powerLawAutoMaxFreqFraction );
1654 auto upper = std::lower_bound( freq.begin(), freq.end(), crossoverFreq );
1655 if( upper == freq.end() )
1657 crossoverFreq = freq.back();
1661 crossoverFreq = *upper;
1664 powerLawMatchFreq = crossoverFreq;
1665 powerLawOnlyAboveFreq = crossoverFreq;
1666 return mx::error_t::noerror;
1669template <
typename realT>
1672 if( requestedNormFreq >
static_cast<realT
>( 0 ) )
1674 return requestedNormFreq;
1677 size_t refIndex = firstPositiveFreqIndex( freq );
1678 if( refIndex < freq.size() && freq[refIndex] >
static_cast<realT
>( 0 ) )
1680 return freq[refIndex];
1683 return static_cast<realT
>( 1 );
1686template <
typename realT>
1688 realT extrapolation,
const std::vector<realT> &freq,
size_t index, realT powerLawIndex, realT powerLawNormFreq )
1690 const realT refFreq = resolvePowerLawNormFreq( freq, powerLawNormFreq );
1691 const realT useFreq = freq[index] >
static_cast<realT
>( 0 ) ? freq[index] : refFreq;
1693 return extrapolation * pow( refFreq / useFreq, powerLawIndex );
1696template <
typename realT>
1698 const std::vector<realT> &freq,
1701 if( psd.empty() || psd.size() != freq.size() )
1703 return std::numeric_limits<realT>::quiet_NaN();
1706 auto upper = std::lower_bound( freq.begin(), freq.end(), targetFreq );
1707 if( upper == freq.begin() )
1712 if( upper == freq.end() )
1717 size_t hi = upper - freq.begin();
1718 if( *upper == targetFreq )
1724 if( freq[hi] == freq[lo] )
1729 realT alpha = ( targetFreq - freq[lo] ) / ( freq[hi] - freq[lo] );
1730 return (
static_cast<realT
>( 1 ) - alpha ) * psd[lo] + alpha * psd[hi];
1733template <
typename realT>
1735 const std::vector<realT> &anchorProcessPsd,
1736 const std::vector<realT> &freq,
1737 realT powerLawIndex,
1738 realT powerLawNormFreq,
1739 realT powerLawMatchFreq,
1740 realT powerLawMatchFallbackWindowHz )
1742 if( powerLawMatchFreq <=
static_cast<realT
>( 0 ) )
1744 return mx::error_t::noerror;
1747 if( anchorProcessPsd.size() != freq.size() || anchorProcessPsd.empty() )
1749 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr,
1750 "Anchor disturbance PSD and frequency grid must have the same size" );
1753 size_t firstPositive = firstPositiveFreqIndex( freq );
1754 if( firstPositive >= freq.size() || freq[firstPositive] <=
static_cast<realT
>( 0 ) )
1756 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
"Frequency grid must contain positive bins" );
1759 if( powerLawMatchFreq < freq[firstPositive] || powerLawMatchFreq > freq.back() )
1761 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
1762 "Power-law match frequency is outside the sampled frequency range" );
1765 const realT tiny = std::numeric_limits<realT>::min();
1766 const realT refFreq = resolvePowerLawNormFreq( freq, powerLawNormFreq );
1767 realT matchPsd = interpolatePsdAtFreq( anchorProcessPsd, freq, powerLawMatchFreq );
1768 if( !std::isfinite( matchPsd ) )
1770 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
1771 "Could not interpolate the disturbance PSD at the match frequency" );
1774 std::vector<realT> localPositivePsd;
1775 const realT localMinFreq = std::max( freq[firstPositive], powerLawMatchFreq - powerLawMatchFallbackWindowHz );
1776 const realT localMaxFreq = std::min( freq.back(), powerLawMatchFreq + powerLawMatchFallbackWindowHz );
1777 size_t localStart = std::lower_bound( freq.begin(), freq.end(), localMinFreq ) - freq.begin();
1778 size_t localEnd = std::upper_bound( freq.begin(), freq.end(), localMaxFreq ) - freq.begin();
1779 for(
size_t n = localStart; n < localEnd; ++n )
1781 if( anchorProcessPsd[n] > tiny )
1783 localPositivePsd.push_back( anchorProcessPsd[n] );
1787 if( !localPositivePsd.empty() )
1789 size_t mid = localPositivePsd.size() / 2;
1790 std::nth_element( localPositivePsd.begin(), localPositivePsd.begin() + mid, localPositivePsd.end() );
1791 realT localMedian = localPositivePsd[mid];
1792 if( localPositivePsd.size() % 2 == 0 )
1794 auto lowerIt = std::max_element( localPositivePsd.begin(), localPositivePsd.begin() + mid );
1795 localMedian =
static_cast<realT
>( 0.5 ) * ( localMedian + *lowerIt );
1798 if( matchPsd <= tiny || matchPsd <
static_cast<realT
>( 0.5 ) * localMedian )
1800 matchPsd = localMedian;
1804 extrapolation = std::max( matchPsd, tiny ) * pow( powerLawMatchFreq / refFreq, powerLawIndex );
1805 return mx::error_t::noerror;
1808template <
typename realT>
1811 return fwhm / (
static_cast<realT
>( 2 ) *
1812 sqrt( pow(
static_cast<realT
>( 2 ),
static_cast<realT
>( 1 ) / beta ) -
static_cast<realT
>( 1 ) ) );
1815template <
typename realT>
1818 const realT alpha = moffatAlphaFromFwhm( fwhm, beta );
1819 return mx::math::func::moffat<realT>( radius,
1820 static_cast<realT
>( 0 ),
1822 static_cast<realT
>( 0 ),
1827template <
typename realT>
1829 realT peakHeight, realT fwhm, realT radius, realT target, realT betaFloor )
1831 if( peakHeight <=
static_cast<realT
>( 0 ) || fwhm <=
static_cast<realT
>( 0 ) || radius <=
static_cast<realT
>( 0 ) ||
1832 target <=
static_cast<realT
>( 0 ) )
1837 realT low = std::max( betaFloor,
static_cast<realT
>( 1.0e-3 ) );
1838 if( moffatValueFromFwhm( radius, peakHeight, fwhm, low ) <=
target )
1843 realT high = std::max(
static_cast<realT
>( 2 ) * low, low +
static_cast<realT
>( 0.5 ) );
1844 while( high <
static_cast<realT
>( 128 ) && moffatValueFromFwhm( radius, peakHeight, fwhm, high ) >
target )
1846 high *=
static_cast<realT
>( 2 );
1849 if( moffatValueFromFwhm( radius, peakHeight, fwhm, high ) >
target )
1854 for(
int n = 0; n < 60; ++n )
1856 realT mid =
static_cast<realT
>( 0.5 ) * ( low + high );
1857 if( moffatValueFromFwhm( radius, peakHeight, fwhm, mid ) >
target )
1870template <
typename realT>
1875 return static_cast<realT
>( 0.5 ) * ( x0 + x1 );
1878 realT alpha = (
target - y0 ) / ( y1 - y0 );
1879 if( alpha <
static_cast<realT
>( 0 ) )
1881 alpha =
static_cast<realT
>( 0 );
1883 else if( alpha >
static_cast<realT
>( 1 ) )
1885 alpha =
static_cast<realT
>( 1 );
1888 return x0 + alpha * ( x1 - x0 );
1891template <
typename realT>
1894 const std::vector<realT> &rawProcessPsd,
1895 const std::vector<realT> &freq,
1898 realT fitBinWidthHz,
1899 realT includeMatchFreqHz )
1901 const realT originalPowerLawIndex = powerLawIndex;
1903 if( rawProcessPsd.size() != freq.size() )
1905 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr,
1906 "Raw disturbance PSD and frequency grid must have the same size" );
1909 if( fitMinFreqHz <=
static_cast<realT
>( 0 ) || fitMaxFreqHz <= fitMinFreqHz ||
1910 fitBinWidthHz <=
static_cast<realT
>( 0 ) )
1912 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
1913 "Invalid power-law exponent fit range or bin width" );
1916 if( fitMinFreqHz >= freq.back() )
1919 powerLawIndex = std::max( originalPowerLawIndex,
static_cast<realT
>( 0 ) );
1920 return mx::error_t::noerror;
1923 const realT tiny = std::numeric_limits<realT>::min();
1924 std::vector<realT> x;
1925 std::vector<realT> y;
1927 size_t idx = std::lower_bound( freq.begin(), freq.end(), fitMinFreqHz ) - freq.begin();
1928 for( realT binStart = fitMinFreqHz; binStart < fitMaxFreqHz; binStart += fitBinWidthHz )
1930 realT binEnd = std::min( binStart + fitBinWidthHz, fitMaxFreqHz );
1931 realT binCenter =
static_cast<realT
>( 0.5 ) * ( binStart + binEnd );
1932 if( binCenter <=
static_cast<realT
>( 0 ) )
1937 std::vector<realT> logPsd;
1938 while( idx < freq.size() && freq[idx] < binEnd )
1940 if( rawProcessPsd[idx] > tiny )
1942 logPsd.push_back( log10( rawProcessPsd[idx] ) );
1948 if( logPsd.empty() )
1953 size_t mid = logPsd.size() / 2;
1954 std::nth_element( logPsd.begin(), logPsd.begin() + mid, logPsd.end() );
1955 realT medianLogPsd = logPsd[mid];
1956 if( logPsd.size() % 2 == 0 )
1958 auto lowerIt = std::max_element( logPsd.begin(), logPsd.begin() + mid );
1959 medianLogPsd =
static_cast<realT
>( 0.5 ) * ( medianLogPsd + *lowerIt );
1962 x.push_back( log10( binCenter ) );
1963 y.push_back( medianLogPsd );
1966 if( includeMatchFreqHz >
static_cast<realT
>( 0 ) &&
1967 ( includeMatchFreqHz < fitMinFreqHz || includeMatchFreqHz >= fitMaxFreqHz ) )
1969 realT matchPsd = interpolatePsdAtFreq( rawProcessPsd, freq, includeMatchFreqHz );
1970 if( std::isfinite( matchPsd ) && matchPsd > tiny )
1972 x.push_back( log10( includeMatchFreqHz ) );
1973 y.push_back( log10( matchPsd ) );
1977 nBinsUsed = x.size();
1981 powerLawIndex = std::max( originalPowerLawIndex,
static_cast<realT
>( 0 ) );
1982 return mx::error_t::noerror;
1987 for(
size_t n = 0; n < nBinsUsed; ++n )
1993 meanX /=
static_cast<realT
>( nBinsUsed );
1994 meanY /=
static_cast<realT
>( nBinsUsed );
1998 for(
size_t n = 0; n < nBinsUsed; ++n )
2000 realT dx = x[n] - meanX;
2001 realT dy = y[n] - meanY;
2006 if( varX <=
static_cast<realT
>( 0 ) )
2009 powerLawIndex = std::max( originalPowerLawIndex,
static_cast<realT
>( 0 ) );
2010 return mx::error_t::noerror;
2013 powerLawIndex = -covXY / varX;
2014 if( !std::isfinite( powerLawIndex ) )
2017 powerLawIndex = std::max( originalPowerLawIndex,
static_cast<realT
>( 0 ) );
2018 return mx::error_t::noerror;
2021 if( powerLawIndex <
static_cast<realT
>( 0 ) )
2023 powerLawIndex =
static_cast<realT
>( 0 );
2026 return mx::error_t::noerror;
2029template <
typename realT>
2031 realT &extrapolation,
2032 size_t &anchorIndex,
2033 const std::vector<realT> &rawProcessPsd,
2034 const std::vector<realT> &anchorProcessPsd,
2035 const std::vector<realT> &noisePsd,
2036 const std::vector<realT> &freq,
2037 realT powerLawIndex,
2038 realT powerLawNormFreq,
2039 realT powerLawMatchFreq,
2040 realT powerLawMatchFallbackWindowHz,
2041 bool fitPowerLawIndex,
2042 realT powerLawFitMinFreqHz,
2043 realT powerLawFitMaxFreqHz,
2044 realT powerLawFitBinWidthHz,
2045 bool powerLawFitIncludesMatchPoint,
2046 realT *usedPowerLawIndex,
2047 size_t *fitBinsUsed )
2049 if( rawProcessPsd.size() != noisePsd.size() || rawProcessPsd.size() != freq.size() ||
2050 anchorProcessPsd.size() != rawProcessPsd.size() )
2052 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr,
2053 "Raw PSD, anchor PSD, noise PSD, and frequency "
2054 "grid must have the same size" );
2057 if( rawProcessPsd.size() < 2 )
2059 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr,
"At least two frequency bins are required" );
2062 if( powerLawIndex <
static_cast<realT
>( 0 ) && !fitPowerLawIndex )
2064 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
"Power-law index must be non-negative" );
2067 const realT tiny = std::numeric_limits<realT>::min();
2068 size_t localFitBinsUsed = 0;
2069 realT localPowerLawIndex = powerLawIndex;
2070 if( fitPowerLawIndex )
2072 mx::error_t errc = fitPowerLawIndexFromBinnedMedians( localPowerLawIndex,
2076 powerLawFitMinFreqHz,
2077 powerLawFitMaxFreqHz,
2078 powerLawFitBinWidthHz,
2079 powerLawFitIncludesMatchPoint ? powerLawMatchFreq
2080 :
static_cast<realT
>( 0 ) );
2083 localPowerLawIndex = std::max( powerLawIndex,
static_cast<realT
>( 0 ) );
2084 localFitBinsUsed = 0;
2088 const size_t refIndex = firstPositiveFreqIndex( freq );
2089 const realT refFreq = resolvePowerLawNormFreq( freq, powerLawNormFreq );
2091 anchorIndex = std::min( refIndex, freq.size() - 1 );
2094 size_t fMax =
static_cast<size_t>(
static_cast<realT
>( 0.05 ) *
static_cast<realT
>( freq.size() ) );
2097 fMax = std::min<size_t>( freq.size(), 2 );
2100 for(
size_t f = 1; f < fMax; ++f )
2102 if( anchorProcessPsd[f] <=
static_cast<realT
>( 0.1 ) * noisePsd[f] )
2107 extrapolation += log10( anchorProcessPsd[f] * pow( freq[f] / refFreq, localPowerLawIndex ) );
2114 extrapolation = pow(
static_cast<realT
>( 10 ), extrapolation /
static_cast<realT
>( nExtrap ) );
2118 const realT useFreq = freq[refIndex] >
static_cast<realT
>( 0 ) ? freq[refIndex] : refFreq;
2120 std::max( anchorProcessPsd[refIndex] *
static_cast<realT
>( pow( useFreq / refFreq, localPowerLawIndex ) ),
2124 mx::error_t errc = matchPowerLawAtFreq( extrapolation,
2130 powerLawMatchFallbackWindowHz );
2136 errc = buildPowerLawContinuum( continuumPsd, extrapolation, freq, localPowerLawIndex, refFreq );
2142 if( usedPowerLawIndex )
2144 *usedPowerLawIndex = localPowerLawIndex;
2149 *fitBinsUsed = localFitBinsUsed;
2152 return mx::error_t::noerror;
2155template <
typename realT>
2157 realT extrapolation,
2158 const std::vector<realT> &freq,
2159 realT powerLawIndex,
2160 realT powerLawNormFreq )
2162 const realT tiny = std::numeric_limits<realT>::min();
2164 continuumPsd.resize( freq.size() );
2165 for(
size_t n = 0; n < freq.size(); ++n )
2168 std::max( powerLawContinuum( extrapolation, freq, n, powerLawIndex, powerLawNormFreq ), tiny );
2171 return mx::error_t::noerror;
2174template <
typename realT>
2176 const std::vector<realT> &rawProcessPsd,
2177 const std::vector<realT> &continuumPsd,
2181 if( rawProcessPsd.size() != continuumPsd.size() )
2183 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr,
2184 "Raw disturbance PSD and continuum PSD must have the same size" );
2187 if( rawProcessPsd.empty() )
2189 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr,
"At least one frequency bin is required" );
2194 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
"Blend width must be positive" );
2197 const realT tiny = std::numeric_limits<realT>::min();
2198 anchorIndex = std::min( anchorIndex, rawProcessPsd.size() - 1 );
2199 size_t blendEnd = std::min( anchorIndex +
static_cast<size_t>( blendBins ), rawProcessPsd.size() - 1 );
2201 processPsd.resize( rawProcessPsd.size() );
2202 for(
size_t n = 0; n < processPsd.size(); ++n )
2204 if( n <= anchorIndex )
2206 processPsd[n] = std::max( rawProcessPsd[n], tiny );
2210 if( n >= blendEnd || blendEnd == anchorIndex )
2212 processPsd[n] = std::max( continuumPsd[n], tiny );
2216 realT w =
static_cast<realT
>( n - anchorIndex ) /
static_cast<realT
>( blendEnd - anchorIndex );
2217 realT logBlend = (
static_cast<realT
>( 1 ) - w ) * log10( std::max( rawProcessPsd[n], tiny ) ) +
2218 w * log10( std::max( continuumPsd[n], tiny ) );
2219 processPsd[n] = pow(
static_cast<realT
>( 10 ), logBlend );
2222 return mx::error_t::noerror;
2225template <
typename realT>
2227 const std::vector<realT> &rawProcessPsd,
2228 const std::vector<realT> &continuumPsd,
2229 const std::vector<realT> &freq,
2230 realT peakDetectWidthHz,
2231 realT peakDetectFactor,
2232 realT peakDetectBroadFactor,
2233 realT peakDetectMinWidthLog )
2235 if( rawProcessPsd.size() != continuumPsd.size() || rawProcessPsd.size() != freq.size() )
2237 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr,
2238 "Raw disturbance PSD, continuum PSD, and "
2239 "frequency grid must have the same size" );
2242 if( rawProcessPsd.size() < 3 )
2244 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr,
"At least three frequency bins are required" );
2247 if( peakDetectWidthHz <=
static_cast<realT
>( 0 ) )
2249 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
"Peak-detection width must be positive" );
2252 if( peakDetectFactor <=
static_cast<realT
>( 1 ) )
2254 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
"Peak-detection factor must exceed unity" );
2257 if( peakDetectBroadFactor <=
static_cast<realT
>( 1 ) )
2259 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
2260 "Broad peak-detection factor must exceed unity" );
2263 if( peakDetectMinWidthLog <=
static_cast<realT
>( 0 ) )
2265 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
2266 "Broad peak minimum log-width must be positive" );
2269 const realT df = freq[1] - freq[0];
2270 if( df <=
static_cast<realT
>( 0 ) )
2272 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
"Frequency spacing must be positive" );
2275 int win = std::max( 3,
static_cast<int>( std::lround( peakDetectWidthHz / df ) ) );
2281 const realT tiny = std::numeric_limits<realT>::min();
2282 std::vector<realT> logRaw( rawProcessPsd.size() );
2283 for(
size_t n = 0; n < rawProcessPsd.size(); ++n )
2285 logRaw[n] = log10( std::max( rawProcessPsd[n], tiny ) );
2288 std::vector<realT> logSmooth;
2289 std::vector<realT> smoothPsd( rawProcessPsd.size() );
2290 mx::math::vectorSmoothMedian( logSmooth, logRaw, win );
2291 for(
size_t n = 0; n < smoothPsd.size(); ++n )
2293 smoothPsd[n] = pow(
static_cast<realT
>( 10 ), logSmooth[n] );
2296 std::vector<unsigned char> strongMask( rawProcessPsd.size(), 0 );
2297 std::vector<unsigned char> candidateMask( rawProcessPsd.size(), 0 );
2298 for(
size_t n = 1; n < rawProcessPsd.size(); ++n )
2300 if( rawProcessPsd[n] > peakDetectFactor * smoothPsd[n] )
2303 candidateMask[n] = 1;
2306 if( rawProcessPsd[n] > peakDetectBroadFactor * smoothPsd[n] )
2308 candidateMask[n] = 1;
2313 std::vector<unsigned char> peakHasStrong;
2314 bool inPeak =
false;
2315 bool currentHasStrong =
false;
2318 for(
size_t n = 1; n < rawProcessPsd.size(); ++n )
2320 if( candidateMask[n] )
2325 currentHasStrong = ( strongMask[n] != 0 );
2328 currentPeak.
m_end = n;
2333 currentPeak.
m_end = n;
2334 currentHasStrong = currentHasStrong || ( strongMask[n] != 0 );
2335 if( rawProcessPsd[n] > rawProcessPsd[currentPeak.
m_peakIndex] )
2344 peaks.push_back( currentPeak );
2345 peakHasStrong.push_back( currentHasStrong ? 1 : 0 );
2351 peaks.push_back( currentPeak );
2352 peakHasStrong.push_back( currentHasStrong ? 1 : 0 );
2355 std::vector<identifiedPeak1D> validPeaks;
2356 validPeaks.reserve( peaks.size() );
2358 for(
size_t p = 0; p < peaks.size(); ++p )
2369 const realT defaultFwhm = std::max( df,
static_cast<realT
>( peak.
m_end - peak.
m_start + 1 ) * df );
2371 realT leftCross = peak.
m_centerFreq -
static_cast<realT
>( 0.5 ) * defaultFwhm;
2374 if( rawProcessPsd[n - 1] <= halfLevel )
2377 interpolateCrossing( freq[n - 1], rawProcessPsd[n - 1], freq[n], rawProcessPsd[n], halfLevel );
2382 realT rightCross = peak.
m_centerFreq +
static_cast<realT
>( 0.5 ) * defaultFwhm;
2383 for(
size_t n = peak.
m_peakIndex; n + 1 < rawProcessPsd.size(); ++n )
2385 if( rawProcessPsd[n + 1] <= halfLevel )
2388 interpolateCrossing( freq[n], rawProcessPsd[n], freq[n + 1], rawProcessPsd[n + 1], halfLevel );
2393 peak.
m_fwhm = rightCross - leftCross;
2394 if( peak.
m_fwhm <=
static_cast<realT
>( 0 ) )
2396 peak.
m_fwhm = defaultFwhm;
2399 const realT minPositiveFreq = std::max( freq[1], tiny );
2400 realT lowerFreq = std::max( peak.
m_centerFreq -
static_cast<realT
>( 0.5 ) * peak.
m_fwhm, minPositiveFreq );
2401 realT upperFreq = std::max( peak.
m_centerFreq +
static_cast<realT
>( 0.5 ) * peak.
m_fwhm, lowerFreq );
2402 realT logWidth = log10( upperFreq / lowerFreq );
2404 if( !peakHasStrong[p] && logWidth < peakDetectMinWidthLog )
2409 validPeaks.push_back( peak );
2412 peaks.swap( validPeaks );
2413 return mx::error_t::noerror;
2416template <
typename realT>
2419 const std::vector<realT> &sourcePsd,
2420 const std::vector<realT> &continuumPsd,
2421 const std::vector<realT> &freq,
2422 realT peakMoffatBeta )
2424 if( peakModel.size() != sourcePsd.size() || peakModel.size() != continuumPsd.size() ||
2425 peakModel.size() != freq.size() )
2427 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr,
2428 "Peak model, source PSD, continuum PSD, and "
2429 "frequency grid must have the same size" );
2432 if( peak.
m_peakIndex >= peakModel.size() || peak.
m_fwhm <=
static_cast<realT
>( 0 ) )
2434 return mx::error_t::noerror;
2437 realT peakBeta = peakMoffatBeta;
2441 const size_t edge = peak.
m_start - 1;
2443 peakBeta = std::max(
2445 fitMoffatBetaToBoundary( peak.
m_peakHeight, peak.
m_fwhm, radius, continuumPsd[edge], peakMoffatBeta ) );
2450 const size_t edge = peak.
m_end + 1;
2452 peakBeta = std::max(
2454 fitMoffatBetaToBoundary( peak.
m_peakHeight, peak.
m_fwhm, radius, continuumPsd[edge], peakMoffatBeta ) );
2457 const realT alpha = moffatAlphaFromFwhm( peak.
m_fwhm, peakBeta );
2458 realT peakAtCenter = mx::math::func::moffat<realT>( peak.
m_centerFreq,
2459 static_cast<realT
>( 0 ),
2464 if( peakAtCenter <= continuumPsd[peak.
m_peakIndex] )
2466 return mx::error_t::noerror;
2470 while( left > 0 && sourcePsd[left - 1] > continuumPsd[left - 1] )
2476 while( right + 1 < peakModel.size() && sourcePsd[right + 1] > continuumPsd[right + 1] )
2481 for(
size_t n = left; n <= right; ++n )
2483 realT peakValue = mx::math::func::moffat<realT>( freq[n],
2484 static_cast<realT
>( 0 ),
2489 realT measuredExcess = std::max( sourcePsd[n] - continuumPsd[n],
static_cast<realT
>( 0 ) );
2490 if( measuredExcess >
static_cast<realT
>( 0 ) )
2492 peakModel[n] += std::min( peakValue, measuredExcess );
2496 return mx::error_t::noerror;
2499template <
typename realT>
2501 const std::vector<realT> &rawProcessPsd,
2502 const std::vector<realT> &continuumPsd,
2503 const std::vector<realT> &freq,
2504 realT peakDetectWidthHz,
2505 realT peakDetectFactor,
2506 realT peakDetectBroadFactor,
2507 realT peakDetectMinWidthLog,
2508 int peakDetectPasses,
2509 realT peakMoffatBeta )
2511 if( peakDetectPasses < 1 )
2513 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
"Peak-detection passes must be positive" );
2516 const realT tiny = std::numeric_limits<realT>::min();
2517 std::vector<realT> workingExcess( rawProcessPsd.size() );
2518 for(
size_t n = 0; n < workingExcess.size(); ++n )
2520 workingExcess[n] = std::max( rawProcessPsd[n] - continuumPsd[n],
static_cast<realT
>( 0 ) );
2525 for(
int pass = 0; pass < peakDetectPasses; ++pass )
2527 std::vector<realT> workingPsd( rawProcessPsd.size() );
2528 for(
size_t n = 0; n < workingPsd.size(); ++n )
2530 workingPsd[n] = std::max( continuumPsd[n] + workingExcess[n], tiny );
2533 std::vector<identifiedPeak1D> passPeaks;
2534 mx::error_t errc = identifyMoffatPeaks( passPeaks,
2540 peakDetectBroadFactor,
2541 peakDetectMinWidthLog );
2547 if( passPeaks.empty() )
2552 std::vector<realT> passModel( rawProcessPsd.size(),
static_cast<realT
>( 0 ) );
2553 for(
size_t p = 0; p < passPeaks.size(); ++p )
2556 addClippedMoffatPeakExcess( passModel, passPeaks[p], workingPsd, continuumPsd, freq, peakMoffatBeta );
2563 bool subtracted =
false;
2564 for(
size_t n = 0; n < workingExcess.size(); ++n )
2566 realT newExcess = std::max( workingExcess[n] - passModel[n],
static_cast<realT
>( 0 ) );
2567 if( newExcess < workingExcess[n] )
2571 workingExcess[n] = newExcess;
2574 peaks.insert( peaks.end(), passPeaks.begin(), passPeaks.end() );
2581 std::sort( peaks.begin(),
2585 auto newEnd = std::unique( peaks.begin(),
2588 { return a.m_peakIndex == b.m_peakIndex; } );
2589 peaks.erase( newEnd, peaks.end() );
2591 return mx::error_t::noerror;
2594template <
typename realT>
2596 std::vector<identifiedPeak1D> &peaks,
2597 std::vector<unsigned char> &repairMask,
2598 const std::vector<realT> &rawProcessPsd,
2599 const std::vector<realT> &noisePsd,
2600 const std::vector<realT> &continuumPsd,
2601 const std::vector<realT> &freq,
2605 if( rawProcessPsd.size() != noisePsd.size() || rawProcessPsd.size() != continuumPsd.size() ||
2606 rawProcessPsd.size() != freq.size() )
2608 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr,
2609 "Raw disturbance PSD, noise PSD, continuum PSD, "
2610 "and frequency grid must have the same size" );
2613 const realT tiny = std::numeric_limits<realT>::min();
2615 mx::error_t errc = identifyMoffatPeaksMultiPass( peaks,
2630 std::vector<realT> peakModel( rawProcessPsd.size(),
static_cast<realT
>( 0 ) );
2631 for(
size_t p = 0; p < peaks.size(); ++p )
2633 errc = addClippedMoffatPeakExcess( peakModel,
2645 std::vector<realT> highFreqModel( rawProcessPsd.size() );
2646 for(
size_t n = 0; n < highFreqModel.size(); ++n )
2650 highFreqModel[n] = std::max( continuumPsd[n], tiny );
2654 realT measuredExcess = std::max( rawProcessPsd[n] - continuumPsd[n],
static_cast<realT
>( 0 ) );
2655 highFreqModel[n] = std::max( continuumPsd[n] + std::min( peakModel[n], measuredExcess ), tiny );
2658 std::vector<realT> extrapolatedPsd;
2659 errc = blendContinuumAtAnchor( extrapolatedPsd,
2669 processPsd.resize( rawProcessPsd.size() );
2670 repairMask.assign( rawProcessPsd.size(), 0 );
2672 std::min( anchorIndex +
static_cast<size_t>( config.
m_powerLawBlendBins ), rawProcessPsd.size() - 1 );
2673 for(
size_t n = 0; n <= blendEnd; ++n )
2678 for(
size_t p = 0; p < peaks.size(); ++p )
2680 size_t peakStart = std::min( peaks[p].m_start, repairMask.size() - 1 );
2681 size_t peakEnd = std::min( peaks[p].m_end, repairMask.size() - 1 );
2682 for(
size_t n = peakStart; n <= peakEnd; ++n )
2690 for(
size_t n = 0; n < repairMask.size(); ++n )
2699 for(
size_t n = 0; n < processPsd.size(); ++n )
2703 processPsd[n] = std::max( continuumPsd[n], tiny );
2707 if( rawProcessPsd[n] > noisePsd[n] )
2709 processPsd[n] = std::max( rawProcessPsd[n], tiny );
2713 processPsd[n] = std::max( extrapolatedPsd[n], tiny );
2717 return mx::error_t::noerror;
2720template <
typename realT>
2722 std::vector<unsigned char> &repairMask,
2723 const std::vector<realT> &rawProcessPsd,
2724 const std::vector<realT> &noisePsd,
2725 const std::vector<realT> &continuumPsd,
2726 const std::vector<realT> &freq,
2730 if( rawProcessPsd.size() != noisePsd.size() || rawProcessPsd.size() != continuumPsd.size() ||
2731 rawProcessPsd.size() != freq.size() )
2733 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr,
2734 "Raw disturbance PSD, noise PSD, continuum PSD, "
2735 "and frequency grid must have the same size" );
2738 const realT tiny = std::numeric_limits<realT>::min();
2739 std::vector<realT> extrapolatedPsd;
2740 bool hardAutoHandoff =
2743 if( hardAutoHandoff )
2745 extrapolatedPsd = continuumPsd;
2749 mx::error_t errc = blendContinuumAtAnchor( extrapolatedPsd,
2760 processPsd.resize( rawProcessPsd.size() );
2761 repairMask.assign( rawProcessPsd.size(), 1 );
2762 for(
size_t n = 0; n < processPsd.size(); ++n )
2766 processPsd[n] = std::max( continuumPsd[n], tiny );
2769 else if( hardAutoHandoff )
2771 processPsd[n] = std::max( rawProcessPsd[n], tiny );
2773 else if( rawProcessPsd[n] > noisePsd[n] )
2775 processPsd[n] = std::max( rawProcessPsd[n], tiny );
2779 processPsd[n] = std::max( extrapolatedPsd[n], tiny );
2783 return mx::error_t::noerror;
2786template <
typename realT>
2788 realT &extrapolation,
2789 size_t &anchorIndex,
2790 std::vector<unsigned char> &repairMask,
2791 const std::vector<realT> &measuredPsd,
2792 const std::vector<realT> &anchorProcessPsd,
2793 const std::vector<realT> &noisePsd,
2794 const std::vector<realT> &freq,
2796 realT *usedPowerLawIndex,
2797 size_t *fitBinsUsed )
2799 if( measuredPsd.size() != noisePsd.size() || measuredPsd.size() != freq.size() )
2801 return mx::error_report<mx::verbose::d>(
2802 mx::error_t::sizeerr,
2803 "Measured PSD, noise PSD, and frequency grid must have the same size" );
2806 const realT tiny = std::numeric_limits<realT>::min();
2807 std::vector<realT> rawProcessPsd( measuredPsd.size() );
2808 for(
size_t n = 0; n < measuredPsd.size(); ++n )
2810 rawProcessPsd[n] = std::max( measuredPsd[n] - noisePsd[n], tiny );
2813 std::vector<realT> continuumPsd;
2814 mx::error_t errc = estimatePowerLawContinuum( continuumPsd,
2841 if( usedPowerLawIndex !=
nullptr )
2843 exactPowerLawIndex = *usedPowerLawIndex;
2846 errc = matchPowerLawAtFreq( extrapolation,
2852 static_cast<realT
>( 0 ) );
2859 buildPowerLawContinuum( continuumPsd, extrapolation, freq, exactPowerLawIndex, config.
m_powerLawNormFreq );
2866 return buildPowerLawOnlyProcessFromContinuum( processPsd,
2876template <
typename realT>
2878 realT &extrapolation,
2879 std::vector<identifiedPeak1D> &peaks,
2880 std::vector<unsigned char> &repairMask,
2881 const std::vector<realT> &measuredPsd,
2882 const std::vector<realT> &anchorProcessPsd,
2883 const std::vector<realT> &noisePsd,
2884 const std::vector<realT> &freq,
2887 if( measuredPsd.size() != noisePsd.size() || measuredPsd.size() != freq.size() )
2889 return mx::error_report<mx::verbose::d>(
2890 mx::error_t::sizeerr,
2891 "Measured PSD, noise PSD, and frequency grid must have the same size" );
2894 if( measuredPsd.size() < 3 )
2896 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr,
"At least three frequency bins are required" );
2901 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
"Peak Moffat beta must be positive" );
2904 const realT tiny = std::numeric_limits<realT>::min();
2905 std::vector<realT> rawProcessPsd( measuredPsd.size() );
2906 for(
size_t n = 0; n < measuredPsd.size(); ++n )
2908 rawProcessPsd[n] = std::max( measuredPsd[n] - noisePsd[n], tiny );
2911 std::vector<realT> continuumPsd;
2912 size_t anchorIndex = 0;
2913 mx::error_t errc = estimatePowerLawContinuum( continuumPsd,
2934 return buildMoffatProcessFromContinuum( processPsd,
2945template <
typename realT>
2947 realT &extrapolation,
2948 const std::vector<realT> &measuredPsd,
2949 const std::vector<realT> &noisePsd,
2950 const std::vector<realT> &freq,
2951 realT powerLawNormFreq,
2952 realT powerLawMatchFreq,
2953 realT powerLawMatchFallbackWindowHz )
2955 if( measuredPsd.size() != noisePsd.size() || measuredPsd.size() != freq.size() )
2957 return mx::error_report<mx::verbose::d>(
2958 mx::error_t::sizeerr,
2959 "Measured PSD, noise PSD, and frequency grid must have the same size" );
2962 if( measuredPsd.size() < 2 )
2964 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr,
"At least two frequency bins are required" );
2967 const realT tiny = std::numeric_limits<realT>::min();
2968 const size_t refIndex = firstPositiveFreqIndex( freq );
2969 const realT refFreq = resolvePowerLawNormFreq( freq, powerLawNormFreq );
2970 processPsd.resize( measuredPsd.size() );
2971 for(
size_t f = 0; f < measuredPsd.size(); ++f )
2973 processPsd[f] = measuredPsd[f] - noisePsd[f];
2979 size_t fMax =
static_cast<size_t>(
static_cast<realT
>( 0.05 ) *
static_cast<realT
>( freq.size() ) );
2982 fMax = std::min<size_t>( freq.size(), 2 );
2985 for(
size_t f = 1; f < fMax; ++f )
2987 if( processPsd[f] <=
static_cast<realT
>( 0.1 ) * noisePsd[f] )
2992 extrapolation += log10( processPsd[f] * pow( freq[f] / refFreq, c_defaultPowerLawIndex ) );
2998 extrapolation = pow(
static_cast<realT
>( 10 ), extrapolation /
static_cast<realT
>( nExtrap ) );
3002 const realT useFreq = freq[refIndex] >
static_cast<realT
>( 0 ) ? freq[refIndex] : refFreq;
3004 std::max( processPsd[refIndex] *
static_cast<realT
>( pow( useFreq / refFreq, c_defaultPowerLawIndex ) ),
3008 mx::error_t errc = matchPowerLawAtFreq( extrapolation,
3011 c_defaultPowerLawIndex,
3014 powerLawMatchFallbackWindowHz );
3020 std::vector<realT> toSmooth( freq.size() );
3021 std::vector<realT> l10( freq.size() );
3022 std::vector<realT> smoothed( freq.size() );
3023 std::vector<int> smoothWidths( freq.size() );
3025 for(
size_t f = 0; f < freq.size(); ++f )
3027 if( processPsd[f] <
static_cast<realT
>( 0 ) )
3029 const realT useFreq = freq[f] >
static_cast<realT
>( 0 ) ? freq[f] : refFreq;
3030 toSmooth[f] = extrapolation * pow( refFreq / useFreq, c_defaultPowerLawIndex );
3034 toSmooth[f] = processPsd[f];
3037 toSmooth[f] = std::max( toSmooth[f], tiny );
3038 smoothWidths[f] = f < 2 ? 2 : 2 + static_cast<int>(
static_cast<realT
>( f ) /
static_cast<realT
>( 10 ) );
3039 l10[f] = log10( toSmooth[f] );
3042 mx::math::vectorSmoothMean( smoothed, l10, smoothWidths );
3044 for(
size_t f = 0; f < freq.size(); ++f )
3046 smoothed[f] = pow(
static_cast<realT
>( 10 ), smoothed[f] );
3047 if( processPsd[f] < noisePsd[f] )
3049 processPsd[f] = smoothed[f];
3052 processPsd[f] = std::max( processPsd[f], tiny );
3055 return mx::error_t::noerror;
3058template <
typename realT>
3060 const std::vector<realT> &freq,
3061 const std::vector<unsigned char> &repairMask,
3065 realT powerLawIndex )
3067 if( processPsd.size() < 3 )
3069 return mx::error_t::noerror;
3072 if( processPsd.size() != freq.size() )
3074 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr,
3075 "Dropout repair frequency grid must match the disturbance PSD size" );
3078 if( !repairMask.empty() && repairMask.size() != processPsd.size() )
3080 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr,
3081 "Dropout repair mask must match the disturbance PSD size" );
3084 if( gapFactor <=
static_cast<realT
>( 0 ) || gapFactor >=
static_cast<realT
>( 1 ) )
3086 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
3087 "Dropout gap factor must be between 0 and 1" );
3090 if( tinyFactor <=
static_cast<realT
>( 0 ) || tinyFactor >=
static_cast<realT
>( 1 ) )
3092 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
3093 "Dropout tiny factor must be between 0 and 1" );
3096 const realT tiny = std::numeric_limits<realT>::min();
3099 realT refFreq =
static_cast<realT
>( 1 );
3100 for(
size_t n = 0; n < freq.size(); ++n )
3102 if( freq[n] >
static_cast<realT
>( 0 ) )
3109 std::vector<realT> sourcePsd = processPsd;
3110 std::vector<realT> updatedPsd = processPsd;
3111 bool changed =
false;
3113 if( repairMask.empty() || repairMask[0] != 0 )
3115 realT runMax = sourcePsd[0];
3116 for(
size_t end = 0; end + 2 < sourcePsd.size(); ++end )
3118 if( !repairMask.empty() && repairMask[end] == 0 )
3123 runMax = std::max( runMax, sourcePsd[end] );
3125 realT right1 = std::max( sourcePsd[end + 1], tiny );
3126 realT right2 = std::max( sourcePsd[end + 2], tiny );
3127 realT flankMin = std::min( right1, right2 );
3128 realT flankMax = std::max( right1, right2 );
3129 if( runMax >= gapFactor * flankMin )
3134 if( runMax > tinyFactor * flankMax )
3139 realT xLeft = log10( std::max( freq[end + 1], refFreq ) );
3140 realT xRight = log10( std::max( freq[end + 2], refFreq ) );
3141 if( xRight <= xLeft )
3143 xRight = xLeft +
static_cast<realT
>( 1 );
3146 realT yLeft = log10( std::max( sourcePsd[end + 1], tiny ) );
3147 realT yRight = log10( std::max( sourcePsd[end + 2], tiny ) );
3149 for(
size_t fill = 0; fill <= end; ++fill )
3151 realT xFill = log10( std::max( freq[fill], refFreq ) );
3152 realT alpha = ( xFill - xLeft ) / ( xRight - xLeft );
3153 realT fillValue = pow(
static_cast<realT
>( 10 ), yLeft + alpha * ( yRight - yLeft ) );
3154 changed = changed || fillValue != updatedPsd[fill];
3155 updatedPsd[fill] = std::max( fillValue, tiny );
3158 if( updatedPsd.size() > 1 )
3160 changed = changed || updatedPsd[0] != updatedPsd[1];
3161 updatedPsd[0] = updatedPsd[1];
3164 sourcePsd = updatedPsd;
3170 while( n + 1 < sourcePsd.size() )
3172 if( !repairMask.empty() && repairMask[n] == 0 )
3178 realT leftVal = std::max( sourcePsd[n - 1], tiny );
3179 if( sourcePsd[n] >= gapFactor * leftVal )
3185 realT runMax = sourcePsd[n];
3186 bool repaired =
false;
3187 for(
size_t end = n; end + 1 < sourcePsd.size(); ++end )
3189 if( !repairMask.empty() && repairMask[end] == 0 )
3194 runMax = std::max( runMax, sourcePsd[end] );
3196 realT rightVal = std::max( sourcePsd[end + 1], tiny );
3197 realT flankMax = std::max( leftVal, rightVal );
3198 bool canExtend = end + 1 < sourcePsd.size() - 1;
3199 if( canExtend && !repairMask.empty() )
3201 canExtend = repairMask[end + 1] != 0;
3204 if( sourcePsd[end] >= gapFactor * rightVal )
3214 realT flankMin = std::min( leftVal, rightVal );
3215 if( runMax >= gapFactor * flankMin )
3225 if( runMax > tinyFactor * flankMax )
3235 realT xLeft = log10( std::max( freq[n - 1], refFreq ) );
3236 realT xRight = log10( std::max( freq[end + 1], refFreq ) );
3237 if( xRight <= xLeft )
3239 xRight = xLeft +
static_cast<realT
>( 1 );
3242 realT yLeft = log10( std::max( sourcePsd[n - 1], tiny ) );
3243 realT yRight = log10( std::max( sourcePsd[end + 1], tiny ) );
3245 for(
size_t fill = n; fill <= end; ++fill )
3247 realT xFill = log10( std::max( freq[fill], refFreq ) );
3248 realT alpha = ( xFill - xLeft ) / ( xRight - xLeft );
3249 if( alpha <
static_cast<realT
>( 0 ) )
3251 alpha =
static_cast<realT
>( 0 );
3253 else if( alpha >
static_cast<realT
>( 1 ) )
3255 alpha =
static_cast<realT
>( 1 );
3258 realT fillValue = pow(
static_cast<realT
>( 10 ), yLeft + alpha * ( yRight - yLeft ) );
3259 changed = changed || fillValue != updatedPsd[fill];
3260 updatedPsd[fill] = std::max( fillValue, tiny );
3274 if( repairMask.empty() || repairMask.back() != 0 )
3276 realT runMax = sourcePsd.back();
3277 for(
size_t offset = 0; offset + 2 < sourcePsd.size(); ++offset )
3279 size_t start = sourcePsd.size() - 1 - offset;
3280 if( !repairMask.empty() && repairMask[start] == 0 )
3285 runMax = std::max( runMax, sourcePsd[start] );
3287 realT left1 = std::max( sourcePsd[start - 1], tiny );
3288 realT left2 = std::max( sourcePsd[start - 2], tiny );
3289 realT flankMin = std::min( left1, left2 );
3290 realT flankMax = std::max( left1, left2 );
3291 if( runMax >= gapFactor * flankMin )
3296 if( runMax > tinyFactor * flankMax )
3301 realT yRight = log10( std::max( sourcePsd[start - 1], tiny ) );
3303 for(
size_t fill = start; fill < sourcePsd.size(); ++fill )
3305 realT useFreq = std::max( freq[fill], refFreq );
3306 realT lastGoodFreq = std::max( freq[start - 1], refFreq );
3308 pow(
static_cast<realT
>( 10 ), yRight ) * pow( lastGoodFreq / useFreq, powerLawIndex );
3309 changed = changed || fillValue != updatedPsd[fill];
3310 updatedPsd[fill] = std::max( fillValue, tiny );
3313 sourcePsd = updatedPsd;
3320 processPsd.swap( updatedPsd );
3323 return mx::error_t::noerror;
3326template <
typename realT>
3328 const std::vector<realT> &processPsd,
3329 const std::vector<realT> &freq,
3331 realT continuumWidthHz )
3333 if( processPsd.size() != freq.size() )
3335 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr,
3336 "Process PSD and frequency grid must be the same size" );
3339 lpProcessPsd = processPsd;
3340 if( cutoffFreq <=
static_cast<realT
>( 0 ) )
3342 return mx::error_t::noerror;
3345 if( continuumWidthHz <=
static_cast<realT
>( 0 ) )
3347 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
"LP continuum width must be positive" );
3350 if( freq.size() < 2 )
3352 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr,
"At least two frequency bins are required" );
3355 auto cutIt = std::lower_bound( freq.begin(), freq.end(), cutoffFreq );
3356 if( cutIt == freq.end() )
3358 return mx::error_t::noerror;
3361 size_t cutIndex = cutIt - freq.begin();
3362 if( cutIndex >= lpProcessPsd.size() )
3364 return mx::error_t::noerror;
3367 const realT df = freq[1] - freq[0];
3368 if( df <=
static_cast<realT
>( 0 ) )
3370 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
"Frequency spacing must be positive" );
3373 int win = std::max( 3,
static_cast<int>( std::lround( continuumWidthHz / df ) ) );
3379 const realT tiny = std::numeric_limits<realT>::min();
3380 std::vector<realT> logTail( lpProcessPsd.size() - cutIndex );
3381 for(
size_t n = cutIndex; n < processPsd.size(); ++n )
3383 logTail[n - cutIndex] = log10( std::max( processPsd[n], tiny ) );
3386 std::vector<realT> logMedianTail;
3387 std::vector<realT> logContinuumTail;
3388 mx::math::vectorSmoothMedian( logMedianTail, logTail, win );
3389 mx::math::vectorSmoothMean( logContinuumTail, logMedianTail, win );
3391 for(
size_t n = cutIndex; n < lpProcessPsd.size(); ++n )
3394 std::max(
static_cast<realT
>( pow(
static_cast<realT
>( 10 ), logContinuumTail[n - cutIndex] ) ), tiny );
3397 return mx::error_t::noerror;
size_t m_dropoutMaxBins
The maximum repaired dropout-run length in bins.
realT m_powerLawFitMinFreqHz
The low edge of the exponent-fit range.
static constexpr const char * c_defaultPowerLawCrossoverMode
The default crossover-selection mode for the power-law handoff.
static mx::error_t applyLpContinuum(std::vector< realT > &lpProcessPsd, const std::vector< realT > &processPsd, const std::vector< realT > &freq, realT cutoffFreq, realT continuumWidthHz)
Replace all LP content above a cutoff with a smoothed continuum.
realT m_lpContinuumWidthHz
The LP-only continuum smoothing width in Hz.
realT m_dropoutGapFactor
The threshold used to identify dropout bins.
static mx::error_t buildPowerLawContinuum(std::vector< realT > &continuumPsd, realT extrapolation, const std::vector< realT > &freq, realT powerLawIndex, realT powerLawNormFreq)
Populate a sampled power-law continuum from a resolved normalization.
std::string m_noiseEstimateStatistic
int m_peakDetectPasses
The number of iterative peak-detection passes.
static constexpr realT c_defaultPowerLawMatchFallbackWindowHz
The default half-width of the local match-frequency fallback window.
std::string m_powerLawCrossoverMode
How the power-law match/cutoff frequencies were chosen.
size_t m_peakIndex
The PSD bin containing the detected peak maximum.
realT m_noiseEstimateLowFreqMaxHz
static constexpr realT c_defaultPowerLawMatchFreq
The default disabled explicit power-law match frequency.
static constexpr bool c_defaultPowerLawFitIncludesMatchPoint
The default choice to include the match point in the exponent fit.
static realT moffatValueFromFwhm(realT radius, realT peakHeight, realT fwhm, realT beta)
Evaluate a zero-background Moffat profile from its height, FWHM, and beta.
static mx::error_t identifyMoffatPeaksMultiPass(std::vector< identifiedPeak1D > &peaks, const std::vector< realT > &rawProcessPsd, const std::vector< realT > &continuumPsd, const std::vector< realT > &freq, realT peakDetectWidthHz, realT peakDetectFactor, realT peakDetectBroadFactor, realT peakDetectMinWidthLog, int peakDetectPasses, realT peakMoffatBeta)
static mx::error_t fitPowerLawIndexFromBinnedMedians(realT &powerLawIndex, size_t &nBinsUsed, const std::vector< realT > &rawProcessPsd, const std::vector< realT > &freq, realT fitMinFreqHz, realT fitMaxFreqHz, realT fitBinWidthHz, realT includeMatchFreqHz=static_cast< realT >(0))
realT m_peakDetectMinWidthLog
The minimum accepted broad-peak width in dex.
static constexpr bool c_defaultFitPowerLawIndex
The default choice to keep the power-law exponent fixed.
static mx::error_t estimateProcessPsd(std::vector< realT > &processPsd, realT &extrapolation, const std::vector< realT > &measuredPsd, const std::vector< realT > &noisePsd, const std::vector< realT > &freq, realT powerLawNormFreq, realT powerLawMatchFreq, realT powerLawMatchFallbackWindowHz)
realT m_powerLawFitMaxFreqHz
The high edge of the exponent-fit range.
std::vector< realT > m_lpProcessPsd
The disturbance PSD passed to the LP optimizer.
std::string m_noiseEstimateDomain
The domain used to estimate the flat noise floor.
realT m_powerLawOnlyAboveFreq
Above this frequency, force the extrapolation to be power-law only.
std::vector< realT > m_smoothedProcessPsd
The smoothed but still unextrapolated OL disturbance PSD.
static constexpr realT c_defaultPeakDetectBroadFactor
The default broad-peak detection factor.
realT m_powerLawMatchFallbackWindowHz
static constexpr realT c_defaultPowerLawOnlyAboveFreq
static constexpr realT c_defaultPeakDetectWidthHz
The default wide smoothing width used by peak detection.
realT m_centerFreq
The detected peak center frequency in Hz.
static constexpr const char * c_defaultNoiseEstimateStatistic
The default statistic used to estimate the flat noise floor.
realT m_peakHeight
The peak height above the continuum PSD.
size_t m_powerLawAnchorIndex
The last frequency bin used to anchor the continuum.
static constexpr realT c_defaultClSignificanceThreshold
The default raw-CL significance threshold multiplier.
size_t m_powerLawFitBinsUsed
The number of populated median bins used in the exponent fit.
static constexpr realT c_defaultPeakDetectFactor
The default strong-peak detection factor.
static constexpr realT c_defaultPeakDetectMinWidthLog
The default minimum accepted broad-peak width in dex.
realT m_noiseFloor
The fitted flat noise floor.
static mx::error_t blendContinuumAtAnchor(std::vector< realT > &processPsd, const std::vector< realT > &rawProcessPsd, const std::vector< realT > &continuumPsd, size_t anchorIndex, int blendBins)
static constexpr realT c_defaultPowerLawAutoSmoothWidthHz
The default median-smoothing width used by the automatic crossover finder.
static constexpr realT c_defaultDropoutTinyFactor
std::vector< identifiedPeak1D > m_peaks
The peaks detected by the Moffat extrapolator.
static realT fitMoffatBetaToBoundary(realT peakHeight, realT fwhm, realT radius, realT target, realT betaFloor)
static mx::error_t addClippedMoffatPeakExcess(std::vector< realT > &peakModel, const identifiedPeak1D &peak, const std::vector< realT > &sourcePsd, const std::vector< realT > &continuumPsd, const std::vector< realT > &freq, realT peakMoffatBeta)
Add one clipped Moffat peak excess to a PSD model.
static mx::error_t buildSmoothedProcessPsd(std::vector< realT > &smoothedProcessPsd, const std::vector< realT > &rawProcessPsd, const std::vector< realT > &freq, realT smoothWidthHz)
static constexpr const char * c_defaultNoiseEstimateDomain
The default domain used for noise-floor estimation.
realT m_powerLawNormFreq
The resolved normalization frequency of the power-law model.
realT m_lpContinuumFreq
The LP-only continuum cutoff frequency in Hz.
realT m_peakDetectWidthHz
The wide smoothing width used for peak detection.
static constexpr int c_defaultPowerLawBlendBins
bool m_fitPowerLawIndex
Whether the exponent was requested to be fit from the PSD.
realT m_dropoutGapFactor
The threshold used to identify dropout bins.
realT m_powerLawMatchFallbackWindowHz
The half-width of the local match-frequency fallback window.
std::string m_noiseEstimateRange
Which end of the PSD was used to estimate the flat noise floor.
static realT moffatAlphaFromFwhm(realT fwhm, realT beta)
Invert the Moffat FWHM relation to recover the alpha parameter.
realT m_peakDetectMinWidthLog
The minimum accepted broad-peak width in log-frequency.
static mx::error_t findAutoPowerLawCrossoverFreq(realT &crossoverFreq, const std::vector< realT > &smoothedProcessPsd, const std::vector< realT > &noisePsd, const std::vector< realT > &freq, realT maxFreqFraction)
static constexpr realT c_defaultPowerLawFitBinWidthHz
The default width of each exponent-fit median bin.
realT m_peakMoffatBeta
The minimum Moffat beta used for synthesized peaks.
bool m_fitPowerLawIndex
Whether to fit the power-law exponent from high-frequency bins.
static mx::error_t estimatePowerLawContinuum(std::vector< realT > &continuumPsd, realT &extrapolation, size_t &anchorIndex, const std::vector< realT > &rawProcessPsd, const std::vector< realT > &anchorProcessPsd, const std::vector< realT > &noisePsd, const std::vector< realT > &freq, realT powerLawIndex, realT powerLawNormFreq, realT powerLawMatchFreq, realT powerLawMatchFallbackWindowHz, bool fitPowerLawIndex=false, realT powerLawFitMinFreqHz=c_defaultPowerLawFitMinFreqHz, realT powerLawFitMaxFreqHz=c_defaultPowerLawFitMaxFreqHz, realT powerLawFitBinWidthHz=c_defaultPowerLawFitBinWidthHz, bool powerLawFitIncludesMatchPoint=c_defaultPowerLawFitIncludesMatchPoint, realT *usedPowerLawIndex=nullptr, size_t *fitBinsUsed=nullptr)
std::vector< realT > m_noisePsd
The flat noise PSD estimate.
realT m_dropoutTinyFactor
std::string m_processMethod
The extrapolation method used for the disturbance PSD.
static mx::error_t estimateProcessPsdMoffatPeaks(std::vector< realT > &processPsd, realT &extrapolation, std::vector< identifiedPeak1D > &peaks, std::vector< unsigned char > &repairMask, const std::vector< realT > &measuredPsd, const std::vector< realT > &anchorProcessPsd, const std::vector< realT > &noisePsd, const std::vector< realT > &freq, const processModelConfig &config)
realT m_fwhm
The peak full-width at half maximum in Hz.
realT m_powerLawAutoSmoothWidthHz
std::vector< realT > m_processPsd
The disturbance PSD used for optimization.
realT m_peakDetectBroadFactor
The broad peak-detection factor threshold.
realT m_powerLawOnlyAboveFreq
Above this frequency, force the extrapolation to be power-law only.
static constexpr const char * c_defaultProcessMethod
The default process-method name.
static constexpr realT c_defaultLpContinuumWidthHz
The default LP-continuum smoothing width.
static constexpr realT c_defaultPowerLawFitMinFreqHz
The default low edge of the power-law exponent fit range.
realT m_extrapolation
The continuum normalization at m_powerLawNormFreq.
static realT powerLawContinuum(realT extrapolation, const std::vector< realT > &freq, size_t index, realT powerLawIndex, realT powerLawNormFreq)
Evaluate the extrapolated power-law continuum at one frequency bin.
realT m_powerLawAutoMaxFreqFraction
realT m_powerLawNormFreq
The normalization frequency for the power-law continuum.
std::string m_closedLoopOlEstimateMethod
Which CL-to-OL reconstruction method was used.
std::string m_noiseEstimateStatistic
std::string m_noiseEstimateRange
Which end of the PSD is used to estimate the flat noise floor.
std::vector< realT > m_rawProcessPsd
The unsmoothed, unextrapolated OL disturbance PSD.
realT m_peakDetectBroadFactor
The lower factor used for broad-peak candidates.
static mx::error_t buildMoffatProcessFromContinuum(std::vector< realT > &processPsd, std::vector< identifiedPeak1D > &peaks, std::vector< unsigned char > &repairMask, const std::vector< realT > &rawProcessPsd, const std::vector< realT > &noisePsd, const std::vector< realT > &continuumPsd, const std::vector< realT > &freq, size_t anchorIndex, const processModelConfig &config)
Build the full Moffat-peak disturbance model from a fixed continuum.
realT m_powerLawAutoSmoothWidthHz
realT m_clSignificanceThreshold
static constexpr const char * c_defaultClosedLoopOlEstimateMethod
The default closed-loop to open-loop PSD reconstruction method.
static mx::error_t estimateProcessPsdPowerLawOnly(std::vector< realT > &processPsd, realT &extrapolation, size_t &anchorIndex, std::vector< unsigned char > &repairMask, const std::vector< realT > &measuredPsd, const std::vector< realT > &anchorProcessPsd, const std::vector< realT > &noisePsd, const std::vector< realT > &freq, const processModelConfig &config, realT *usedPowerLawIndex=nullptr, size_t *fitBinsUsed=nullptr)
Build a disturbance PSD from only the extrapolated 1/f^a continuum.
static std::string normalizeNoiseEstimateRange(std::string range)
Normalize a noise-estimation-range name to lowercase hyphenated form.
size_t m_dropoutMaxBins
The maximum dropout-run length repaired by the gap-filling logic.
realT m_powerLawIndex
The power-law exponent used in extrapolation.
static mx::error_t buildPowerLawOnlyProcessFromContinuum(std::vector< realT > &processPsd, std::vector< unsigned char > &repairMask, const std::vector< realT > &rawProcessPsd, const std::vector< realT > &noisePsd, const std::vector< realT > &continuumPsd, const std::vector< realT > &freq, size_t anchorIndex, const processModelConfig &config)
Build the power-law-only disturbance model from a fixed continuum.
static mx::error_t identifyMoffatPeaks(std::vector< identifiedPeak1D > &peaks, const std::vector< realT > &rawProcessPsd, const std::vector< realT > &continuumPsd, const std::vector< realT > &freq, realT peakDetectWidthHz, realT peakDetectFactor, realT peakDetectBroadFactor, realT peakDetectMinWidthLog)
realT m_powerLawAnchorFreq
The frequency where the continuum takes over.
size_t m_end
The last PSD bin in the detected peak region.
realT m_dropoutTinyFactor
static constexpr realT c_defaultPowerLawFitMaxFreqHz
The default high edge of the power-law exponent fit range.
realT m_powerLawFitMaxFreqHz
The high edge of the exponent-fit range.
static mx::error_t estimateNoisePsd(std::vector< realT > &noisePsd, realT &noiseFloor, const std::vector< realT > &measuredPsd, const std::vector< realT > &freq, size_t modeIndex, std::string noiseEstimateRange=c_defaultNoiseEstimateRange, std::string noiseEstimateStatistic=c_defaultNoiseEstimateStatistic, realT noiseEstimateLowFreqMaxHz=c_defaultNoiseEstimateLowFreqMaxHz)
Estimate the flat noise PSD using the configured modalGainOpt statistic.
realT m_powerLawMatchFreq
realT m_peakDetectFactor
The minimum factor above the smoothed PSD for a strong peak.
static realT interpolatePsdAtFreq(const std::vector< realT > &psd, const std::vector< realT > &freq, realT targetFreq)
Linearly interpolate a sampled PSD onto an arbitrary frequency.
static mx::error_t fillProcessPsdDropouts(std::vector< realT > &processPsd, const std::vector< realT > &freq, const std::vector< unsigned char > &repairMask, realT gapFactor, realT tinyFactor, size_t maxGapBins, realT powerLawIndex)
Fill isolated or short dropout runs in a disturbance PSD.
int m_peakDetectPasses
The number of iterative peak-detection passes.
static constexpr size_t c_defaultDropoutMaxBins
The default maximum repaired dropout-run length in bins.
static mx::error_t analyzePsd(processResults &result, const std::vector< realT > &measuredPsd, const std::vector< realT > &freq, size_t modeIndex, const processModelConfig &config, realT lpContinuumFreq=static_cast< realT >(0), realT lpContinuumWidthHz=c_defaultLpContinuumWidthHz, const std::vector< realT > *etfPsd=nullptr, const std::vector< realT > *ntfPsd=nullptr)
Build the noise PSD, disturbance PSD, and LP continuum PSD for one mode.
static constexpr int c_defaultPeakDetectPasses
The default number of subtract-and-redetect peak passes.
static constexpr const char * c_defaultNoiseEstimateRange
The default end of the PSD used for noise-floor estimation.
realT m_powerLawIndex
The power-law exponent in .
static mx::error_t resolvePowerLawCrossoverFrequencies(realT &powerLawMatchFreq, realT &powerLawOnlyAboveFreq, const std::vector< realT > &rawProcessPsd, const std::vector< realT > &smoothedProcessPsd, const std::vector< realT > &noisePsd, const std::vector< realT > &freq, std::string powerLawCrossoverMode, realT powerLawAutoMaxFreqFraction)
std::string m_powerLawCrossoverMode
How the power-law match/cutoff frequencies are chosen.
std::string m_noiseEstimateDomain
The domain used to estimate the flat noise floor.
static std::string normalizePowerLawCrossoverMode(std::string mode)
Normalize a power-law crossover mode name to lowercase hyphenated form.
realT m_noiseEstimateLowFreqMaxHz
static mx::error_t matchPowerLawAtFreq(realT &extrapolation, const std::vector< realT > &anchorProcessPsd, const std::vector< realT > &freq, realT powerLawIndex, realT powerLawNormFreq, realT powerLawMatchFreq, realT powerLawMatchFallbackWindowHz)
bool m_powerLawIndexFitSucceeded
Whether the exponent fit succeeded and was applied.
size_t m_start
The first PSD bin in the detected peak region.
static constexpr realT c_defaultPeakMoffatBeta
The default lower bound on the Moffat beta parameter.
static std::string normalizeNoiseEstimateStatistic(std::string statistic)
Normalize a noise-estimation-statistic name to lowercase hyphenated form.
realT m_powerLawAutoMaxFreqFraction
static std::string normalizeClosedLoopOlEstimateMethod(std::string method)
std::string m_closedLoopOlEstimateMethod
realT m_peakDetectWidthHz
The peak-detection smoothing width.
static size_t firstPositiveFreqIndex(const std::vector< realT > &freq)
Return the index of the first strictly-positive frequency bin.
realT m_powerLawFitBinWidthHz
The width of the exponent-fit median bins.
static constexpr realT c_defaultPowerLawNormFreq
The default auto-selected power-law normalization frequency.
bool m_powerLawFitIncludesMatchPoint
Whether the match point is included directly in the exponent fit.
bool m_powerLawFitIncludesMatchPoint
Whether the exponent fit included the explicit match point.
realT m_powerLawFitMinFreqHz
The low edge of the exponent-fit range.
static constexpr realT c_defaultClMinSignificantFraction
static constexpr realT c_defaultNoiseEstimateLowFreqMaxHz
The default disabled maximum frequency for low-frequency noise estimation.
realT m_powerLawMatchFreq
static constexpr realT c_defaultPowerLawAutoMaxFreqFraction
static realT resolvePowerLawNormFreq(const std::vector< realT > &freq, realT requestedNormFreq)
realT m_peakDetectFactor
The strong peak-detection factor threshold.
realT m_peakMoffatBeta
The Moffat beta used for synthesized peaks.
realT m_clMinSignificantFraction
static constexpr realT c_defaultPowerLawIndex
The default 1/f^a exponent.
std::string m_method
The extrapolation method name.
int m_powerLawBlendBins
The blend width used at the power-law anchor.
static realT interpolateCrossing(realT x0, realT y0, realT x1, realT y1, realT target)
static std::string normalizeNoiseEstimateDomain(std::string domain)
Normalize a noise-estimation-domain name to lowercase hyphenated form.
realT m_powerLawFitBinWidthHz
The width of the exponent-fit median bins.
static constexpr realT c_defaultDropoutGapFactor
The default dropout-detection threshold factor.
Description of one detected spectral peak.
Configuration of the disturbance-PSD extrapolation model.
Results of modal PSD noise estimation and disturbance extrapolation.