API
 
Loading...
Searching...
No Matches
modalPsdProcessor.hpp
Go to the documentation of this file.
1/** \file modalPsdProcessor.hpp
2 * \brief Header-only template class for modal PSD noise estimation and
3 * disturbance extrapolation.
4 *
5 * \author OpenAI Codex
6 */
7
8#ifndef modalPsdProcessor_hpp
9#define modalPsdProcessor_hpp
10
11#include <algorithm>
12#include <cctype>
13#include <cmath>
14#include <limits>
15#include <string>
16#include <vector>
17
18#include <mx/error/error.hpp>
19#include <mx/math/func/moffat.hpp>
20#include <mx/math/vectorUtils.hpp>
21
22namespace MagAOX
23{
24namespace app
25{
26
27/// Header-only helper for modal PSD noise estimation, extrapolation, and LP
28/// continuum shaping.
29template <typename realT>
31{
32 public:
33 /// The default process-method name.
34 static constexpr const char *c_defaultProcessMethod = "legacy";
35
36 /// The default `1/f^a` exponent.
37 static constexpr realT c_defaultPowerLawIndex = static_cast<realT>( 8.0 / 3.0 );
38
39 /// The default auto-selected power-law normalization frequency.
40 static constexpr realT c_defaultPowerLawNormFreq = static_cast<realT>( 0 );
41
42 /// The default disabled explicit power-law match frequency.
43 static constexpr realT c_defaultPowerLawMatchFreq = static_cast<realT>( 0 );
44
45 /// The default domain used for noise-floor estimation.
46 static constexpr const char *c_defaultNoiseEstimateDomain = "open-loop";
47
48 /// The default end of the PSD used for noise-floor estimation.
49 static constexpr const char *c_defaultNoiseEstimateRange = "high-freq";
50
51 /// The default disabled maximum frequency for low-frequency noise estimation.
52 static constexpr realT c_defaultNoiseEstimateLowFreqMaxHz = static_cast<realT>( 0 );
53
54 /// The default statistic used to estimate the flat noise floor.
55 static constexpr const char *c_defaultNoiseEstimateStatistic = "percentile";
56
57 /// The default closed-loop to open-loop PSD reconstruction method.
58 static constexpr const char *c_defaultClosedLoopOlEstimateMethod = "etf-only";
59
60 /// The default half-width of the local match-frequency fallback window.
61 static constexpr realT c_defaultPowerLawMatchFallbackWindowHz = static_cast<realT>( 5 );
62
63 /// The default crossover-selection mode for the power-law handoff.
64 static constexpr const char *c_defaultPowerLawCrossoverMode = "manual";
65
66 /// The default median-smoothing width used by the automatic crossover finder.
67 static constexpr realT c_defaultPowerLawAutoSmoothWidthHz = static_cast<realT>( 50 );
68
69 /// The default fraction of the maximum sampled frequency searched by the
70 /// automatic crossover finder. Set to 0 to disable the cap.
71 static constexpr realT c_defaultPowerLawAutoMaxFreqFraction = static_cast<realT>( 0.4 );
72
73 /// The default choice to keep the power-law exponent fixed.
74 static constexpr bool c_defaultFitPowerLawIndex = false;
75
76 /// The default choice to include the match point in the exponent fit.
77 static constexpr bool c_defaultPowerLawFitIncludesMatchPoint = true;
78
79 /// The default low edge of the power-law exponent fit range.
80 static constexpr realT c_defaultPowerLawFitMinFreqHz = static_cast<realT>( 100 );
81
82 /// The default high edge of the power-law exponent fit range.
83 static constexpr realT c_defaultPowerLawFitMaxFreqHz = static_cast<realT>( 1000 );
84
85 /// The default width of each exponent-fit median bin.
86 static constexpr realT c_defaultPowerLawFitBinWidthHz = static_cast<realT>( 100 );
87
88 /// The default number of bins used to blend between the raw PSD and the
89 /// extrapolated continuum.
90 static constexpr int c_defaultPowerLawBlendBins = 5;
91
92 /// The default wide smoothing width used by peak detection.
93 static constexpr realT c_defaultPeakDetectWidthHz = static_cast<realT>( 50 );
94
95 /// The default strong-peak detection factor.
96 static constexpr realT c_defaultPeakDetectFactor = static_cast<realT>( 5 );
97
98 /// The default broad-peak detection factor.
99 static constexpr realT c_defaultPeakDetectBroadFactor = static_cast<realT>( 3 );
100
101 /// The default minimum accepted broad-peak width in dex.
102 static constexpr realT c_defaultPeakDetectMinWidthLog = static_cast<realT>( 0.02 );
103
104 /// The default lower bound on the Moffat beta parameter.
105 static constexpr realT c_defaultPeakMoffatBeta = static_cast<realT>( 6 );
106
107 /// The default number of subtract-and-redetect peak passes.
108 static constexpr int c_defaultPeakDetectPasses = 2;
109
110 /// The default dropout-detection threshold factor.
111 static constexpr realT c_defaultDropoutGapFactor = static_cast<realT>( 0.2 );
112
113 /// The default factor below the local good-bin scale that a candidate run
114 /// must reach to be considered a true dropout.
115 static constexpr realT c_defaultDropoutTinyFactor = static_cast<realT>( 1e-6 );
116
117 /// The default maximum repaired dropout-run length in bins.
118 static constexpr size_t c_defaultDropoutMaxBins = 4;
119
120 /// The default raw-CL significance threshold multiplier.
121 static constexpr realT c_defaultClSignificanceThreshold = static_cast<realT>( 1.1 );
122
123 /// The default minimum fraction of significant raw-CL bins required to keep
124 /// processing a mode.
125 static constexpr realT c_defaultClMinSignificantFraction = static_cast<realT>( 0.05 );
126
127 /// The default LP-continuum smoothing width.
128 static constexpr realT c_defaultLpContinuumWidthHz = static_cast<realT>( 25 );
129
130 /// The default disabled cutoff above which the extrapolation is pure power
131 /// law only.
132 static constexpr realT c_defaultPowerLawOnlyAboveFreq = static_cast<realT>( 0 );
133
134 /// Configuration of the disturbance-PSD extrapolation model.
136 {
137 /// The extrapolation method name.
139
140 /// The power-law exponent \f$a\f$ in \f$1/f^a\f$.
142
143 /// The normalization frequency for the power-law continuum.
145
146 /// The frequency where the power law is forced to match the raw disturbance
147 /// PSD.
149
150 /// The domain used to estimate the flat noise floor.
152
153 /// Which end of the PSD is used to estimate the flat noise floor.
155
156 /// The maximum frequency in Hz used by the low-frequency noise estimate, or
157 /// 0 to disable.
159
160 /// Which statistic is used to estimate the flat noise floor from the
161 /// selected bins.
163
164 /// How to reconstruct the OL PSD from a CL PSD when estimating noise in CL
165 /// space.
167
168 /// The half-width of the local fallback window used when the match point is
169 /// in a trough.
171
172 /// How the power-law match/cutoff frequencies are chosen.
174
175 /// The median-smoothing width used when automatically locating the
176 /// crossover.
178
179 /// The fraction of the maximum sampled frequency searched when
180 /// automatically locating the crossover. Set to 0 to disable the cap.
182
183 /// Whether to fit the power-law exponent from high-frequency bins.
185
186 /// Above this frequency, force the extrapolation to be power-law only.
188
189 /// Whether the match point is included directly in the exponent fit.
191
192 /// The low edge of the exponent-fit range.
194
195 /// The high edge of the exponent-fit range.
197
198 /// The width of the exponent-fit median bins.
200
201 /// The number of bins used to blend between the measured PSD and the
202 /// extrapolated continuum.
204
205 /// The wide smoothing width used for peak detection.
207
208 /// The minimum factor above the smoothed PSD for a strong peak.
210
211 /// The lower factor used for broad-peak candidates.
213
214 /// The minimum accepted broad-peak width in log-frequency.
216
217 /// The number of iterative peak-detection passes.
219
220 /// The minimum Moffat beta used for synthesized peaks.
222
223 /// The threshold used to identify dropout bins.
225
226 /// The factor below the local good-bin scale that a candidate run must
227 /// reach to be considered a true dropout.
229
230 /// The maximum dropout-run length repaired by the gap-filling logic.
232
233 /// The multiplier above the fitted raw-CL noise floor required for a bin
234 /// to be considered significant.
236
237 /// The minimum fraction of raw-CL bins that must be significant for a
238 /// mode to remain active.
240 };
241
242 /// Description of one detected spectral peak.
244 {
245 /// The first PSD bin in the detected peak region.
246 size_t m_start{ 0 };
247
248 /// The last PSD bin in the detected peak region.
249 size_t m_end{ 0 };
250
251 /// The PSD bin containing the detected peak maximum.
252 size_t m_peakIndex{ 0 };
253
254 /// The detected peak center frequency in Hz.
255 realT m_centerFreq{ 0 };
256
257 /// The peak height above the continuum PSD.
258 realT m_peakHeight{ 0 };
259
260 /// The peak full-width at half maximum in Hz.
261 realT m_fwhm{ 0 };
262 };
263
264 /// Results of modal PSD noise estimation and disturbance extrapolation.
266 {
267 /// The extrapolation method used for the disturbance PSD.
269
270 /// The fitted flat noise floor.
271 realT m_noiseFloor{ 0 };
272
273 /// The continuum normalization at `m_powerLawNormFreq`.
274 realT m_extrapolation{ 0 };
275
276 /// The power-law exponent used in extrapolation.
278
279 /// The resolved normalization frequency of the power-law model.
281
282 /// The frequency where the power law is forced to match the disturbance
283 /// PSD.
285
286 /// The domain used to estimate the flat noise floor.
288
289 /// Which end of the PSD was used to estimate the flat noise floor.
291
292 /// The maximum frequency in Hz used by the low-frequency noise estimate, or
293 /// 0 if disabled.
295
296 /// Which statistic was used to estimate the flat noise floor from the
297 /// selected bins.
299
300 /// Which CL-to-OL reconstruction method was used.
302
303 /// The half-width of the local match-frequency fallback window.
305
306 /// How the power-law match/cutoff frequencies were chosen.
308
309 /// The median-smoothing width used when automatically locating the
310 /// crossover.
312
313 /// The fraction of the maximum sampled frequency searched when
314 /// automatically locating the crossover. Set to 0 to disable the cap.
316
317 /// Whether the exponent was requested to be fit from the PSD.
319
320 /// Above this frequency, force the extrapolation to be power-law only.
322
323 /// Whether the exponent fit succeeded and was applied.
325
326 /// Whether the exponent fit included the explicit match point.
328
329 /// The low edge of the exponent-fit range.
331
332 /// The high edge of the exponent-fit range.
334
335 /// The width of the exponent-fit median bins.
337
338 /// The number of populated median bins used in the exponent fit.
340
341 /// The last frequency bin used to anchor the continuum.
343
344 /// The frequency where the continuum takes over.
346
347 /// The blend width used at the power-law anchor.
349
350 /// The peak-detection smoothing width.
352
353 /// The strong peak-detection factor threshold.
355
356 /// The broad peak-detection factor threshold.
358
359 /// The minimum accepted broad-peak width in dex.
361
362 /// The number of iterative peak-detection passes.
364
365 /// The Moffat beta used for synthesized peaks.
367
368 /// The threshold used to identify dropout bins.
370
371 /// The factor below the local good-bin scale that a candidate run must
372 /// reach to be considered a true dropout.
374
375 /// The maximum repaired dropout-run length in bins.
377
378 /// The LP-only continuum cutoff frequency in Hz.
380
381 /// The LP-only continuum smoothing width in Hz.
383
384 /// The flat noise PSD estimate.
385 std::vector<realT> m_noisePsd;
386
387 /// The disturbance PSD used for optimization.
388 std::vector<realT> m_processPsd;
389
390 /// The disturbance PSD passed to the LP optimizer.
391 std::vector<realT> m_lpProcessPsd;
392
393 /// The unsmoothed, unextrapolated OL disturbance PSD.
394 std::vector<realT> m_rawProcessPsd;
395
396 /// The smoothed but still unextrapolated OL disturbance PSD.
397 std::vector<realT> m_smoothedProcessPsd;
398
399 /// The peaks detected by the Moffat extrapolator.
400 std::vector<identifiedPeak1D> m_peaks;
401 };
402
403 /// Build the noise PSD, disturbance PSD, and LP continuum PSD for one mode.
404 static mx::error_t
405 analyzePsd( processResults &result, /**< [out] the populated process-model results */
406 const std::vector<realT> &measuredPsd, /**< [in] the measured one-sided PSD */
407 const std::vector<realT> &freq, /**< [in] the one-sided frequency grid */
408 size_t modeIndex, /**< [in] the zero-based mode index */
409 const processModelConfig &config, /**< [in] the disturbance-PSD configuration */
410 realT lpContinuumFreq = static_cast<realT>( 0 ), /**< [in] the LP continuum cutoff */
411 realT lpContinuumWidthHz = c_defaultLpContinuumWidthHz, /**< [in] LP smoothing width */
412 const std::vector<realT> *etfPsd = nullptr, /**< [in] optional CL ETF^2 correction */
413 const std::vector<realT> *ntfPsd = nullptr /**< [in] optional CL NTF^2 correction */
414 );
415
416 /// Estimate the flat noise PSD using the configured modalGainOpt statistic.
417 static mx::error_t estimateNoisePsd(
418 std::vector<realT> &noisePsd, /**< [out] the flat noise PSD estimate */
419 realT &noiseFloor, /**< [out] the fitted noise floor */
420 const std::vector<realT> &measuredPsd, /**< [in] the measured one-sided PSD */
421 const std::vector<realT> &freq, /**< [in] the one-sided frequency grid */
422 size_t modeIndex, /**< [in] the zero-based mode index */
423 std::string noiseEstimateRange = c_defaultNoiseEstimateRange, /**< [in] which PSD end to use */
424 std::string noiseEstimateStatistic = c_defaultNoiseEstimateStatistic, /**< [in] how to summarize the
425 selected bins */
426 realT noiseEstimateLowFreqMaxHz = c_defaultNoiseEstimateLowFreqMaxHz /**< [in] optional low-frequency
427 upper limit */
428 );
429
430 /// Replace all LP content above a cutoff with a smoothed continuum.
431 static mx::error_t applyLpContinuum( std::vector<realT> &lpProcessPsd, /**< [out] the LP disturbance PSD */
432 const std::vector<realT> &processPsd, /**< [in] the nominal disturbance PSD */
433 const std::vector<realT> &freq, /**< [in] the one-sided frequency grid */
434 realT cutoffFreq, /**< [in] the continuum cutoff frequency */
435 realT continuumWidthHz /**< [in] the smoothing width */
436 );
437
438 protected:
439 /// Return the index of the first strictly-positive frequency bin.
440 static size_t firstPositiveFreqIndex( const std::vector<realT> &freq /**< [in] the one-sided frequency grid */ );
441
442 /// Normalize a noise-estimation-domain name to lowercase hyphenated form.
443 static std::string normalizeNoiseEstimateDomain( std::string domain /**< [in] the requested domain name */ );
444
445 /// Normalize a noise-estimation-range name to lowercase hyphenated form.
446 static std::string normalizeNoiseEstimateRange( std::string range /**< [in] the requested PSD-end selector */ );
447
448 /// Normalize a noise-estimation-statistic name to lowercase hyphenated form.
449 static std::string
450 normalizeNoiseEstimateStatistic( std::string statistic /**< [in] the requested noise-fit statistic */ );
451
452 /// Normalize a CL-to-OL PSD reconstruction method name to lowercase
453 /// hyphenated form.
454 static std::string
455 normalizeClosedLoopOlEstimateMethod( std::string method /**< [in] the requested CL-to-OL method */ );
456
457 /// Normalize a power-law crossover mode name to lowercase hyphenated form.
458 static std::string normalizePowerLawCrossoverMode( std::string mode /**< [in] the requested crossover mode */ );
459
460 /// Resolve the power-law normalization frequency, defaulting to the first
461 /// positive bin.
462 static realT resolvePowerLawNormFreq( const std::vector<realT> &freq, /**< [in] the one-sided frequency grid */
463 realT requestedNormFreq /**< [in] the requested normalization frequency */
464 );
465
466 /// Median-smooth a disturbance PSD in log space while preserving the original
467 /// sampling.
468 static mx::error_t
469 buildSmoothedProcessPsd( std::vector<realT> &smoothedProcessPsd, /**< [out] the smoothed disturbance PSD */
470 const std::vector<realT> &rawProcessPsd, /**< [in] the raw disturbance PSD */
471 const std::vector<realT> &freq, /**< [in] the one-sided frequency grid */
472 realT smoothWidthHz /**< [in] the median-smoothing width in Hz */
473 );
474
475 /// Determine the automatic power-law crossover from a median-smoothed
476 /// disturbance PSD.
477 static mx::error_t findAutoPowerLawCrossoverFreq(
478 realT &crossoverFreq, /**< [out] the resolved crossover frequency */
479 const std::vector<realT> &smoothedProcessPsd, /**< [in] the smoothed disturbance PSD */
480 const std::vector<realT> &noisePsd, /**< [in] the flat noise PSD */
481 const std::vector<realT> &freq, /**< [in] the one-sided frequency grid */
482 realT maxFreqFraction /**< [in] the maximum searched frequency as a fraction of the
483 sampled maximum */
484 );
485
486 /// Resolve effective power-law match and cutoff frequencies for manual or
487 /// automatic crossover modes.
488 static mx::error_t resolvePowerLawCrossoverFrequencies(
489 realT &powerLawMatchFreq, /**< [in.out] match frequency */
490 realT &powerLawOnlyAboveFreq, /**< [in.out] cutoff frequency */
491 const std::vector<realT> &rawProcessPsd, /**< [in] raw OL disturbance PSD */
492 const std::vector<realT> &smoothedProcessPsd, /**< [in] smoothed OL disturbance PSD */
493 const std::vector<realT> &noisePsd, /**< [in] OL noise PSD */
494 const std::vector<realT> &freq, /**< [in] frequency grid */
495 std::string powerLawCrossoverMode, /**< [in] mode */
496 realT powerLawAutoMaxFreqFraction /**< [in] the maximum searched frequency as a fraction of the
497 sampled maximum */
498 );
499
500 /// Evaluate the extrapolated power-law continuum at one frequency bin.
501 static realT powerLawContinuum( realT extrapolation, /**< [in] the continuum normalization */
502 const std::vector<realT> &freq, /**< [in] the one-sided frequency grid */
503 size_t index, /**< [in] the bin index to evaluate */
504 realT powerLawIndex, /**< [in] the power-law exponent */
505 realT powerLawNormFreq /**< [in] the continuum normalization frequency */
506 );
507
508 /// Linearly interpolate a sampled PSD onto an arbitrary frequency.
509 static realT interpolatePsdAtFreq( const std::vector<realT> &psd, /**< [in] the sampled PSD values */
510 const std::vector<realT> &freq, /**< [in] the sampled frequencies */
511 realT targetFreq /**< [in] the desired interpolation frequency */
512 );
513
514 /// Force the power-law normalization so the model matches the disturbance PSD
515 /// at a chosen frequency.
516 static mx::error_t
517 matchPowerLawAtFreq( realT &extrapolation, /**< [in.out] the continuum normalization */
518 const std::vector<realT> &anchorProcessPsd, /**< [in] the PSD used for continuum anchoring */
519 const std::vector<realT> &freq, /**< [in] the one-sided frequency grid */
520 realT powerLawIndex, /**< [in] the power-law exponent */
521 realT powerLawNormFreq, /**< [in] the continuum normalization frequency */
522 realT powerLawMatchFreq, /**< [in] the desired match frequency */
523 realT powerLawMatchFallbackWindowHz /**< [in] the local fallback
524 half-width */
525 );
526
527 /// Invert the Moffat FWHM relation to recover the alpha parameter.
528 static realT moffatAlphaFromFwhm( realT fwhm, /**< [in] the desired full-width at half-maximum */
529 realT beta /**< [in] the Moffat beta parameter */
530 );
531
532 /// Evaluate a zero-background Moffat profile from its height, FWHM, and beta.
533 static realT moffatValueFromFwhm( realT radius, /**< [in] the distance from the peak center */
534 realT peakHeight, /**< [in] the peak height above the continuum */
535 realT fwhm, /**< [in] the full-width at half maximum */
536 realT beta /**< [in] the Moffat beta parameter */
537 );
538
539 /// Find the smallest beta that drives a Moffat wing down to a target level at
540 /// a target radius.
541 static realT fitMoffatBetaToBoundary( realT peakHeight, /**< [in] the peak height above the continuum */
542 realT fwhm, /**< [in] the full-width at half maximum */
543 realT radius, /**< [in] the target boundary radius */
544 realT target, /**< [in] the desired level at that boundary */
545 realT betaFloor /**< [in] the minimum allowed beta */
546 );
547
548 /// Linearly interpolate the x-position where a profile crosses a target
549 /// level.
550 static realT interpolateCrossing( realT x0, /**< [in] the first x-coordinate */
551 realT y0, /**< [in] the first y-coordinate */
552 realT x1, /**< [in] the second x-coordinate */
553 realT y1, /**< [in] the second y-coordinate */
554 realT target /**< [in] the y-level to interpolate */
555 );
556
557 /// Fit the power-law exponent from binned median log PSD values over a
558 /// selected frequency range.
559 static mx::error_t fitPowerLawIndexFromBinnedMedians(
560 realT &powerLawIndex, /**< [out] the fitted exponent */
561 size_t &nBinsUsed, /**< [out] the number of populated bins */
562 const std::vector<realT> &rawProcessPsd, /**< [in] disturbance PSD */
563 const std::vector<realT> &freq, /**< [in] the one-sided frequency grid */
564 realT fitMinFreqHz, /**< [in] the low edge of the fit range */
565 realT fitMaxFreqHz, /**< [in] the high edge of the fit range */
566 realT fitBinWidthHz, /**< [in] the width of the median bins */
567 realT includeMatchFreqHz = static_cast<realT>( 0 ) /**< [in] optional explicit match point */
568 );
569
570 /// Estimate the `1/f^a` continuum used by the power-law and Moffat
571 /// extrapolators.
572 static mx::error_t estimatePowerLawContinuum(
573 std::vector<realT> &continuumPsd, /**< [out] the continuum PSD */
574 realT &extrapolation, /**< [out] the continuum normalization */
575 size_t &anchorIndex, /**< [out] the last bin used to anchor the fit */
576 const std::vector<realT> &rawProcessPsd, /**< [in] raw disturbance PSD */
577 const std::vector<realT> &anchorProcessPsd, /**< [in] smoothed disturbance PSD used for anchoring */
578 const std::vector<realT> &noisePsd, /**< [in] the flat noise PSD */
579 const std::vector<realT> &freq, /**< [in] the one-sided frequency grid */
580 realT powerLawIndex, /**< [in] the power-law exponent */
581 realT powerLawNormFreq, /**< [in] the normalization frequency */
582 realT powerLawMatchFreq, /**< [in] the optional match frequency */
583 realT powerLawMatchFallbackWindowHz, /**< [in] the match fallback
584 half-width */
585 bool fitPowerLawIndex = false, /**< [in] whether to fit the exponent */
586 realT powerLawFitMinFreqHz = c_defaultPowerLawFitMinFreqHz, /**< [in] fit low edge */
587 realT powerLawFitMaxFreqHz = c_defaultPowerLawFitMaxFreqHz, /**< [in] fit high edge */
588 realT powerLawFitBinWidthHz = c_defaultPowerLawFitBinWidthHz, /**< [in] fit bin width */
589 bool powerLawFitIncludesMatchPoint = c_defaultPowerLawFitIncludesMatchPoint, /**< [in] include match point
590 */
591 realT *usedPowerLawIndex = nullptr, /**< [out] the exponent actually used */
592 size_t *fitBinsUsed = nullptr /**< [out] the number of populated fit bins */
593 );
594
595 /// Populate a sampled power-law continuum from a resolved normalization.
596 static mx::error_t buildPowerLawContinuum( std::vector<realT> &continuumPsd, /**< [out] the sampled continuum PSD */
597 realT extrapolation, /**< [in] the continuum normalization */
598 const std::vector<realT> &freq, /**< [in] the one-sided frequency grid */
599 realT powerLawIndex, /**< [in] the power-law exponent */
600 realT powerLawNormFreq /**< [in] the normalization frequency */
601 );
602
603 /// Blend from a continuum model to the measured disturbance PSD at the anchor
604 /// point.
605 static mx::error_t
606 blendContinuumAtAnchor( std::vector<realT> &processPsd, /**< [out] the blended disturbance PSD */
607 const std::vector<realT> &rawProcessPsd, /**< [in] measured disturbance PSD */
608 const std::vector<realT> &continuumPsd, /**< [in] extrapolated continuum PSD */
609 size_t anchorIndex, /**< [in] the last bin where the continuum anchors */
610 int blendBins /**< [in] the number of handoff bins */
611 );
612
613 /// Identify peaks using strong-threshold or broad-but-wide discriminants
614 /// above a smoothed PSD.
615 static mx::error_t
616 identifyMoffatPeaks( std::vector<identifiedPeak1D> &peaks, /**< [out] the detected peaks */
617 const std::vector<realT> &rawProcessPsd, /**< [in] the raw disturbance PSD */
618 const std::vector<realT> &continuumPsd, /**< [in] the continuum PSD model */
619 const std::vector<realT> &freq, /**< [in] the one-sided frequency grid */
620 realT peakDetectWidthHz, /**< [in] the wide smoothing width */
621 realT peakDetectFactor, /**< [in] the factor above smooth for strong peaks
622 */
623 realT peakDetectBroadFactor, /**< [in] the lower factor for broad
624 candidates */
625 realT peakDetectMinWidthLog /**< [in] the minimum accepted broad-peak
626 width */
627 );
628
629 /// Add one clipped Moffat peak excess to a PSD model.
630 static mx::error_t
631 addClippedMoffatPeakExcess( std::vector<realT> &peakModel, /**< [in.out] the accumulated peak model */
632 const identifiedPeak1D &peak, /**< [in] the detected peak */
633 const std::vector<realT> &sourcePsd, /**< [in] the source PSD used to bound the peak */
634 const std::vector<realT> &continuumPsd, /**< [in] the continuum PSD */
635 const std::vector<realT> &freq, /**< [in] the one-sided frequency grid */
636 realT peakMoffatBeta /**< [in] the minimum Moffat beta */
637 );
638
639 /// Identify peaks iteratively, subtracting each pass's modeled peaks from the
640 /// residual.
641 static mx::error_t
642 identifyMoffatPeaksMultiPass( std::vector<identifiedPeak1D> &peaks, /**< [out] the detected peaks */
643 const std::vector<realT> &rawProcessPsd, /**< [in] the raw disturbance PSD */
644 const std::vector<realT> &continuumPsd, /**< [in] the continuum PSD model */
645 const std::vector<realT> &freq, /**< [in] the one-sided frequency grid */
646 realT peakDetectWidthHz, /**< [in] the wide smoothing width */
647 realT peakDetectFactor, /**< [in] the factor above smooth */
648 realT peakDetectBroadFactor, /**< [in] the broad-peak factor */
649 realT peakDetectMinWidthLog, /**< [in] the minimum log-width */
650 int peakDetectPasses, /**< [in] the number of iterative passes */
651 realT peakMoffatBeta /**< [in] the minimum Moffat beta */
652 );
653
654 /// Build the full Moffat-peak disturbance model from a fixed continuum.
655 static mx::error_t
656 buildMoffatProcessFromContinuum( std::vector<realT> &processPsd, /**< [out] the disturbance PSD */
657 std::vector<identifiedPeak1D> &peaks, /**< [out] detected peaks */
658 std::vector<unsigned char> &repairMask, /**< [out] repair-eligible bins */
659 const std::vector<realT> &rawProcessPsd, /**< [in] raw disturbance PSD */
660 const std::vector<realT> &noisePsd, /**< [in] the flat noise PSD */
661 const std::vector<realT> &continuumPsd, /**< [in] the continuum PSD */
662 const std::vector<realT> &freq, /**< [in] the one-sided frequency grid */
663 size_t anchorIndex, /**< [in] the continuum handoff bin */
664 const processModelConfig &config /**< [in] the process configuration */
665 );
666
667 /// Build the power-law-only disturbance model from a fixed continuum.
668 static mx::error_t
669 buildPowerLawOnlyProcessFromContinuum( std::vector<realT> &processPsd, /**< [out] the disturbance PSD */
670 std::vector<unsigned char> &repairMask, /**< [out] repair-eligible bins */
671 const std::vector<realT> &rawProcessPsd, /**< [in] raw disturbance PSD */
672 const std::vector<realT> &noisePsd, /**< [in] the flat noise PSD */
673 const std::vector<realT> &continuumPsd, /**< [in] the continuum PSD */
674 const std::vector<realT> &freq, /**< [in] the one-sided frequency grid */
675 size_t anchorIndex, /**< [in] the continuum handoff bin */
676 const processModelConfig &config /**< [in] the process configuration */
677 );
678
679 /// Build a disturbance PSD from only the extrapolated `1/f^a` continuum.
680 static mx::error_t
681 estimateProcessPsdPowerLawOnly( std::vector<realT> &processPsd, /**< [out] the disturbance PSD */
682 realT &extrapolation, /**< [out] the power-law continuum anchor */
683 size_t &anchorIndex, /**< [out] the last frequency bin used to anchor the
684 fit */
685 std::vector<unsigned char> &repairMask, /**< [out] bins eligible for repair */
686 const std::vector<realT> &measuredPsd, /**< [in] the measured one-sided PSD */
687 const std::vector<realT> &anchorProcessPsd, /**< [in] the smoothed disturbance PSD
688 used for anchoring */
689 const std::vector<realT> &noisePsd, /**< [in] the flat noise PSD */
690 const std::vector<realT> &freq, /**< [in] the one-sided frequency grid */
691 const processModelConfig &config, /**< [in] the disturbance-PSD configuration */
692 realT *usedPowerLawIndex = nullptr, /**< [out] the exponent actually used */
693 size_t *fitBinsUsed = nullptr /**< [out] the number of populated fit bins */
694 );
695
696 /// Build a disturbance PSD by combining a `1/f^a` continuum with detected
697 /// Moffat peaks.
698 static mx::error_t
699 estimateProcessPsdMoffatPeaks( std::vector<realT> &processPsd, /**< [out] the disturbance PSD */
700 realT &extrapolation, /**< [out] the power-law continuum anchor */
701 std::vector<identifiedPeak1D> &peaks, /**< [out] the detected peaks */
702 std::vector<unsigned char> &repairMask, /**< [out] bins eligible for repair */
703 const std::vector<realT> &measuredPsd, /**< [in] the measured one-sided PSD */
704 const std::vector<realT> &anchorProcessPsd, /**< [in] the smoothed disturbance PSD
705 used for anchoring */
706 const std::vector<realT> &noisePsd, /**< [in] the flat noise PSD */
707 const std::vector<realT> &freq, /**< [in] the one-sided frequency grid */
708 const processModelConfig &config /**< [in] the extrapolation configuration */
709 );
710
711 /// Build the disturbance PSD used for optimization from the measured PSD and
712 /// the flat noise estimate.
713 static mx::error_t
714 estimateProcessPsd( std::vector<realT> &processPsd, /**< [out] the disturbance PSD */
715 realT &extrapolation, /**< [out] the low-frequency extrapolation anchor */
716 const std::vector<realT> &measuredPsd, /**< [in] the measured one-sided PSD */
717 const std::vector<realT> &noisePsd, /**< [in] the flat noise PSD estimate */
718 const std::vector<realT> &freq, /**< [in] the one-sided frequency grid */
719 realT powerLawNormFreq, /**< [in] the power-law normalization frequency */
720 realT powerLawMatchFreq, /**< [in] the optional power-law match frequency */
721 realT powerLawMatchFallbackWindowHz /**< [in] the local match fallback
722 half-width */
723 );
724
725 /// Fill isolated or short dropout runs in a disturbance PSD.
726 static mx::error_t
727 fillProcessPsdDropouts( std::vector<realT> &processPsd, /**< [in.out] the disturbance PSD */
728 const std::vector<realT> &freq, /**< [in] the one-sided frequency grid */
729 const std::vector<unsigned char> &repairMask, /**< [in] repair-eligible bins */
730 realT gapFactor, /**< [in] the threshold used to identify dropout bins */
731 realT tinyFactor, /**< [in] the factor below the local good-bin scale
732 required for a true dropout */
733 size_t maxGapBins, /**< [in] the maximum repaired gap length */
734 realT powerLawIndex /**< [in] the power-law exponent used to continue a trailing gap */
735 );
736};
737
738template <typename realT>
740 const std::vector<realT> &measuredPsd,
741 const std::vector<realT> &freq,
742 size_t modeIndex,
743 const processModelConfig &config,
744 realT lpContinuumFreq,
745 realT lpContinuumWidthHz,
746 const std::vector<realT> *etfPsd,
747 const std::vector<realT> *ntfPsd )
748{
749 if( measuredPsd.size() != freq.size() )
750 {
751 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr,
752 "Measured PSD and frequency grid must be the same size" );
753 }
754
755 if( measuredPsd.size() < 2 )
756 {
757 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr, "At least two frequency bins are required" );
758 }
759
760 result.m_processMethod = config.m_method;
761 result.m_powerLawIndex = config.m_powerLawIndex;
762 result.m_powerLawNormFreq = resolvePowerLawNormFreq( freq, config.m_powerLawNormFreq );
764 result.m_noiseEstimateDomain = normalizeNoiseEstimateDomain( config.m_noiseEstimateDomain );
765 result.m_noiseEstimateRange = normalizeNoiseEstimateRange( config.m_noiseEstimateRange );
767 result.m_noiseEstimateStatistic = normalizeNoiseEstimateStatistic( config.m_noiseEstimateStatistic );
768 result.m_closedLoopOlEstimateMethod = normalizeClosedLoopOlEstimateMethod( config.m_closedLoopOlEstimateMethod );
770 result.m_powerLawCrossoverMode = normalizePowerLawCrossoverMode( config.m_powerLawCrossoverMode );
775 result.m_powerLawIndexFitSucceeded = false;
780 result.m_powerLawFitBinsUsed = 0;
787 result.m_peakMoffatBeta = config.m_peakMoffatBeta;
790 result.m_dropoutMaxBins = config.m_dropoutMaxBins;
791 result.m_lpContinuumFreq = lpContinuumFreq;
792 result.m_lpContinuumWidthHz = lpContinuumWidthHz;
793
794 if( config.m_dropoutGapFactor <= static_cast<realT>( 0 ) || config.m_dropoutGapFactor >= static_cast<realT>( 1 ) )
795 {
796 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
797 "Dropout gap factor must be between 0 and 1" );
798 }
799
800 if( config.m_dropoutTinyFactor <= static_cast<realT>( 0 ) || config.m_dropoutTinyFactor >= static_cast<realT>( 1 ) )
801 {
802 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
803 "Dropout tiny factor must be between 0 and 1" );
804 }
805
806 if( config.m_powerLawOnlyAboveFreq < static_cast<realT>( 0 ) )
807 {
808 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
809 "Power-law-only-above frequency must be non-negative" );
810 }
811
812 if( config.m_powerLawMatchFallbackWindowHz < static_cast<realT>( 0 ) )
813 {
814 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
815 "Power-law match fallback window must be non-negative" );
816 }
817
818 if( config.m_powerLawBlendBins < 0 )
819 {
820 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg, "Power-law blend bins must be non-negative" );
821 }
822
823 if( config.m_dropoutMaxBins < 1 )
824 {
825 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg, "Dropout max bins must be at least one" );
826 }
827
828 if( config.m_fitPowerLawIndex && ( config.m_powerLawFitMinFreqHz <= static_cast<realT>( 0 ) ||
830 config.m_powerLawFitBinWidthHz <= static_cast<realT>( 0 ) ) )
831 {
832 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
833 "Invalid power-law exponent fit range or bin width" );
834 }
835
836 if( result.m_noiseEstimateDomain != "open-loop" && result.m_noiseEstimateDomain != "closed-loop-pre-xfer" )
837 {
838 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
839 "Unknown noise-estimation domain: " + result.m_noiseEstimateDomain );
840 }
841
842 if( result.m_noiseEstimateRange != "high-freq" && result.m_noiseEstimateRange != "low-freq" )
843 {
844 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
845 "Unknown noise-estimation range: " + result.m_noiseEstimateRange );
846 }
847
848 if( result.m_noiseEstimateStatistic != "percentile" && result.m_noiseEstimateStatistic != "minimum" )
849 {
850 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
851 "Unknown noise-estimation statistic: " +
853 }
854
855 if( result.m_noiseEstimateLowFreqMaxHz < static_cast<realT>( 0 ) )
856 {
857 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
858 "Low-frequency noise-estimate maximum must be non-negative" );
859 }
860
861 if( result.m_closedLoopOlEstimateMethod != "etf-only" && result.m_closedLoopOlEstimateMethod != "ntf-aware" )
862 {
863 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
864 "Unknown closed-loop OL estimate method: " +
866 }
867
868 if( result.m_powerLawCrossoverMode != "manual" && result.m_powerLawCrossoverMode != "auto-smoothed-crossing" )
869 {
870 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
871 "Unknown power-law crossover mode: " +
873 }
874
875 if( result.m_powerLawCrossoverMode == "auto-smoothed-crossing" &&
876 result.m_powerLawAutoSmoothWidthHz <= static_cast<realT>( 0 ) )
877 {
878 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
879 "Automatic power-law crossover smoothing width must be positive" );
880 }
881
882 if( result.m_powerLawAutoMaxFreqFraction < static_cast<realT>( 0 ) ||
883 result.m_powerLawAutoMaxFreqFraction > static_cast<realT>( 1 ) )
884 {
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" );
888 }
889
890 const std::vector<realT> &noiseEstimatePsd = measuredPsd;
891
892 mx::error_t errc = estimateNoisePsd( result.m_noisePsd,
893 result.m_noiseFloor,
894 noiseEstimatePsd,
895 freq,
896 modeIndex,
900 if( !!errc )
901 {
902 return errc;
903 }
904
905 std::vector<realT> processMeasuredPsd = measuredPsd;
906 std::vector<realT> processNoisePsd = result.m_noisePsd;
907 if( result.m_noiseEstimateDomain == "closed-loop-pre-xfer" )
908 {
909 if( etfPsd == nullptr )
910 {
911 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
912 "Closed-loop noise estimation requires an ETF PSD" );
913 }
914
915 if( etfPsd->size() != measuredPsd.size() )
916 {
917 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr, "ETF PSD must match the measured PSD size" );
918 }
919
920 if( result.m_closedLoopOlEstimateMethod == "ntf-aware" )
921 {
922 if( ntfPsd == nullptr )
923 {
924 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
925 "NTF-aware closed-loop noise estimation requires an NTF PSD" );
926 }
927
928 if( ntfPsd->size() != measuredPsd.size() )
929 {
930 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr,
931 "NTF PSD must match the measured PSD size" );
932 }
933 }
934
935 const realT tiny = std::numeric_limits<realT>::min();
936 for( size_t n = 0; n < measuredPsd.size(); ++n )
937 {
938 const realT useEtf = std::max( ( *etfPsd )[n], tiny );
939 realT closedLoopNoise = result.m_noisePsd[n];
940 if( result.m_closedLoopOlEstimateMethod == "ntf-aware" )
941 {
942 closedLoopNoise = result.m_noisePsd[n] * std::max( ( *ntfPsd )[n], tiny );
943 }
944
945 const realT rawClosedLoop = std::max( measuredPsd[n] - closedLoopNoise, tiny );
946 const realT openLoopProcess = rawClosedLoop / useEtf;
947
948 // Keep the comparison noise PSD in the same CL-noise domain as the
949 // fitted/published noise floor. The OL disturbance estimate is still
950 // formed from (CL - noise) / ETF above.
951 processNoisePsd[n] = std::max( closedLoopNoise, tiny );
952 processMeasuredPsd[n] = openLoopProcess + processNoisePsd[n];
953 }
954 }
955
956 processModelConfig effectiveConfig = config;
957 effectiveConfig.m_method = result.m_processMethod;
958 effectiveConfig.m_noiseEstimateDomain = result.m_noiseEstimateDomain;
959 effectiveConfig.m_noiseEstimateRange = result.m_noiseEstimateRange;
960 effectiveConfig.m_noiseEstimateStatistic = result.m_noiseEstimateStatistic;
962 effectiveConfig.m_powerLawCrossoverMode = result.m_powerLawCrossoverMode;
965
966 const realT tiny = std::numeric_limits<realT>::min();
967 result.m_rawProcessPsd.resize( processMeasuredPsd.size() );
968 for( size_t n = 0; n < processMeasuredPsd.size(); ++n )
969 {
970 result.m_rawProcessPsd[n] = std::max( processMeasuredPsd[n] - processNoisePsd[n], tiny );
971 }
972
973 if( effectiveConfig.m_method == "power-law-only" || effectiveConfig.m_method == "moffat-peaks" )
974 {
975 errc = fillProcessPsdDropouts( result.m_rawProcessPsd,
976 freq,
977 {},
978 effectiveConfig.m_dropoutGapFactor,
979 effectiveConfig.m_dropoutTinyFactor,
980 effectiveConfig.m_dropoutMaxBins,
981 effectiveConfig.m_powerLawIndex );
982 if( !!errc )
983 {
984 return errc;
985 }
986
987 for( size_t n = 0; n < processMeasuredPsd.size(); ++n )
988 {
989 processMeasuredPsd[n] = result.m_rawProcessPsd[n] + processNoisePsd[n];
990 }
991 }
992
993 if( effectiveConfig.m_powerLawAutoSmoothWidthHz > static_cast<realT>( 0 ) )
994 {
995 errc = buildSmoothedProcessPsd( result.m_smoothedProcessPsd,
996 result.m_rawProcessPsd,
997 freq,
998 effectiveConfig.m_powerLawAutoSmoothWidthHz );
999 if( !!errc )
1000 {
1001 return errc;
1002 }
1003 }
1004 else
1005 {
1006 result.m_smoothedProcessPsd = result.m_rawProcessPsd;
1007 }
1008
1009 errc = resolvePowerLawCrossoverFrequencies( effectiveConfig.m_powerLawMatchFreq,
1010 effectiveConfig.m_powerLawOnlyAboveFreq,
1011 result.m_rawProcessPsd,
1012 result.m_smoothedProcessPsd,
1013 processNoisePsd,
1014 freq,
1015 effectiveConfig.m_powerLawCrossoverMode,
1016 effectiveConfig.m_powerLawAutoMaxFreqFraction );
1017 if( !!errc )
1018 {
1019 return errc;
1020 }
1021
1022 result.m_powerLawMatchFreq = effectiveConfig.m_powerLawMatchFreq;
1023 result.m_powerLawOnlyAboveFreq = effectiveConfig.m_powerLawOnlyAboveFreq;
1024
1025 result.m_peaks.clear();
1026 std::vector<unsigned char> processRepairMask;
1027
1028 if( effectiveConfig.m_method == "legacy" )
1029 {
1030 result.m_powerLawIndex = c_defaultPowerLawIndex;
1031 result.m_powerLawAnchorIndex = 0;
1032 result.m_powerLawAnchorFreq = 0;
1033 errc = estimateProcessPsd( result.m_processPsd,
1034 result.m_extrapolation,
1035 processMeasuredPsd,
1036 processNoisePsd,
1037 freq,
1038 effectiveConfig.m_powerLawNormFreq,
1039 effectiveConfig.m_powerLawMatchFreq,
1040 effectiveConfig.m_powerLawMatchFallbackWindowHz );
1041 if( !!errc )
1042 {
1043 return errc;
1044 }
1045 }
1046 else if( effectiveConfig.m_method == "power-law-only" )
1047 {
1048 realT usedPowerLawIndex = effectiveConfig.m_powerLawIndex;
1049 size_t fitBinsUsed = 0;
1050 errc = estimateProcessPsdPowerLawOnly( result.m_processPsd,
1051 result.m_extrapolation,
1052 result.m_powerLawAnchorIndex,
1053 processRepairMask,
1054 processMeasuredPsd,
1055 result.m_smoothedProcessPsd,
1056 processNoisePsd,
1057 freq,
1058 effectiveConfig,
1059 &usedPowerLawIndex,
1060 &fitBinsUsed );
1061 if( !!errc )
1062 {
1063 return errc;
1064 }
1065
1066 result.m_powerLawAnchorFreq =
1067 result.m_powerLawAnchorIndex < freq.size() ? freq[result.m_powerLawAnchorIndex] : static_cast<realT>( 0 );
1068 result.m_powerLawIndex = usedPowerLawIndex;
1069 result.m_powerLawIndexFitSucceeded = effectiveConfig.m_fitPowerLawIndex && fitBinsUsed > 0;
1070 result.m_powerLawFitBinsUsed = fitBinsUsed;
1071 }
1072 else if( effectiveConfig.m_method == "moffat-peaks" )
1073 {
1074 size_t anchorIndex = 0;
1075 errc = estimateProcessPsdMoffatPeaks( result.m_processPsd,
1076 result.m_extrapolation,
1077 result.m_peaks,
1078 processRepairMask,
1079 processMeasuredPsd,
1080 result.m_smoothedProcessPsd,
1081 processNoisePsd,
1082 freq,
1083 effectiveConfig );
1084 if( !!errc )
1085 {
1086 return errc;
1087 }
1088
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 )
1092 {
1093 rawProcessPsd[n] = std::max( processMeasuredPsd[n] - processNoisePsd[n], tiny );
1094 }
1095
1096 std::vector<realT> continuumPsd;
1097 realT usedPowerLawIndex = effectiveConfig.m_powerLawIndex;
1098 size_t fitBinsUsed = 0;
1099 errc = estimatePowerLawContinuum( continuumPsd,
1100 result.m_extrapolation,
1101 anchorIndex,
1102 rawProcessPsd,
1103 result.m_smoothedProcessPsd,
1104 processNoisePsd,
1105 freq,
1106 effectiveConfig.m_powerLawIndex,
1107 effectiveConfig.m_powerLawNormFreq,
1108 effectiveConfig.m_powerLawMatchFreq,
1109 effectiveConfig.m_powerLawMatchFallbackWindowHz,
1110 effectiveConfig.m_fitPowerLawIndex,
1111 effectiveConfig.m_powerLawFitMinFreqHz,
1112 effectiveConfig.m_powerLawFitMaxFreqHz,
1113 effectiveConfig.m_powerLawFitBinWidthHz,
1114 effectiveConfig.m_powerLawFitIncludesMatchPoint,
1115 &usedPowerLawIndex,
1116 &fitBinsUsed );
1117 if( !!errc )
1118 {
1119 return errc;
1120 }
1121
1122 result.m_powerLawAnchorIndex = anchorIndex;
1123 result.m_powerLawAnchorFreq =
1124 result.m_powerLawAnchorIndex < freq.size() ? freq[result.m_powerLawAnchorIndex] : static_cast<realT>( 0 );
1125 result.m_powerLawIndex = usedPowerLawIndex;
1126 result.m_powerLawIndexFitSucceeded = effectiveConfig.m_fitPowerLawIndex && fitBinsUsed > 0;
1127 result.m_powerLawFitBinsUsed = fitBinsUsed;
1128 }
1129 else
1130 {
1131 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
1132 "Unknown process method: " + effectiveConfig.m_method );
1133 }
1134
1135 if( effectiveConfig.m_method == "power-law-only" || effectiveConfig.m_method == "moffat-peaks" )
1136 {
1137 errc = fillProcessPsdDropouts( result.m_processPsd,
1138 freq,
1139 processRepairMask,
1140 effectiveConfig.m_dropoutGapFactor,
1141 effectiveConfig.m_dropoutTinyFactor,
1142 effectiveConfig.m_dropoutMaxBins,
1143 result.m_powerLawIndex );
1144 if( !!errc )
1145 {
1146 return errc;
1147 }
1148
1149 if( effectiveConfig.m_powerLawMatchFreq > static_cast<realT>( 0 ) )
1150 {
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 )
1154 {
1155 rawProcessPsd[n] = std::max( processMeasuredPsd[n] - processNoisePsd[n], tiny );
1156 }
1157
1158 std::vector<realT> continuumPsd;
1159 size_t rematchAnchorIndex = 0;
1160 errc = estimatePowerLawContinuum( continuumPsd,
1161 result.m_extrapolation,
1162 rematchAnchorIndex,
1163 rawProcessPsd,
1164 result.m_smoothedProcessPsd,
1165 processNoisePsd,
1166 freq,
1167 result.m_powerLawIndex,
1168 effectiveConfig.m_powerLawNormFreq,
1169 static_cast<realT>( 0 ),
1170 effectiveConfig.m_powerLawMatchFallbackWindowHz,
1171 false );
1172 if( !!errc )
1173 {
1174 return errc;
1175 }
1176
1177 errc = matchPowerLawAtFreq( result.m_extrapolation,
1178 result.m_smoothedProcessPsd,
1179 freq,
1180 result.m_powerLawIndex,
1181 effectiveConfig.m_powerLawNormFreq,
1182 effectiveConfig.m_powerLawMatchFreq,
1183 normalizePowerLawCrossoverMode( effectiveConfig.m_powerLawCrossoverMode ) ==
1184 "auto-smoothed-crossing"
1185 ? static_cast<realT>( 0 )
1186 : effectiveConfig.m_powerLawMatchFallbackWindowHz );
1187 if( !!errc )
1188 {
1189 return errc;
1190 }
1191
1192 errc = buildPowerLawContinuum( continuumPsd,
1193 result.m_extrapolation,
1194 freq,
1195 result.m_powerLawIndex,
1196 effectiveConfig.m_powerLawNormFreq );
1197 if( !!errc )
1198 {
1199 return errc;
1200 }
1201
1202 result.m_powerLawAnchorIndex = rematchAnchorIndex;
1203 result.m_powerLawAnchorFreq = result.m_powerLawAnchorIndex < freq.size()
1204 ? freq[result.m_powerLawAnchorIndex]
1205 : static_cast<realT>( 0 );
1206
1207 if( effectiveConfig.m_method == "power-law-only" )
1208 {
1209 errc = buildPowerLawOnlyProcessFromContinuum( result.m_processPsd,
1210 processRepairMask,
1211 rawProcessPsd,
1212 processNoisePsd,
1213 continuumPsd,
1214 freq,
1215 result.m_powerLawAnchorIndex,
1216 effectiveConfig );
1217 }
1218 else
1219 {
1220 errc = buildMoffatProcessFromContinuum( result.m_processPsd,
1221 result.m_peaks,
1222 processRepairMask,
1223 rawProcessPsd,
1224 processNoisePsd,
1225 continuumPsd,
1226 freq,
1227 result.m_powerLawAnchorIndex,
1228 effectiveConfig );
1229 }
1230 if( !!errc )
1231 {
1232 return errc;
1233 }
1234
1235 errc = fillProcessPsdDropouts( result.m_processPsd,
1236 freq,
1237 processRepairMask,
1238 effectiveConfig.m_dropoutGapFactor,
1239 effectiveConfig.m_dropoutTinyFactor,
1240 effectiveConfig.m_dropoutMaxBins,
1241 result.m_powerLawIndex );
1242 if( !!errc )
1243 {
1244 return errc;
1245 }
1246 }
1247 }
1248
1249 errc = applyLpContinuum( result.m_lpProcessPsd, result.m_processPsd, freq, lpContinuumFreq, lpContinuumWidthHz );
1250 if( !!errc )
1251 {
1252 return errc;
1253 }
1254
1255 return mx::error_t::noerror;
1256}
1257
1258template <typename realT>
1259mx::error_t modalPsdProcessor<realT>::estimateNoisePsd( std::vector<realT> &noisePsd,
1260 realT &noiseFloor,
1261 const std::vector<realT> &measuredPsd,
1262 const std::vector<realT> &freq,
1263 size_t modeIndex,
1264 std::string noiseEstimateRange,
1265 std::string noiseEstimateStatistic,
1266 realT noiseEstimateLowFreqMaxHz )
1267{
1268 if( measuredPsd.size() < 2 || measuredPsd.size() != freq.size() )
1269 {
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" );
1272 }
1273
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" )
1279 {
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 ) )
1283 {
1284 size_t cappedF1 = f0;
1285 while( cappedF1 < measuredPsd.size() && freq[cappedF1] <= noiseEstimateLowFreqMaxHz )
1286 {
1287 ++cappedF1;
1288 }
1289
1290 f1 = std::max( f0 + static_cast<size_t>( 1 ), std::min( f1, cappedF1 ) );
1291 }
1292 }
1293 else if( noiseEstimateRange != "high-freq" )
1294 {
1295 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
1296 "Unknown noise-estimation range: " + noiseEstimateRange );
1297 }
1298
1299 if( f1 <= f0 )
1300 {
1301 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr, "PSD is too short for noise fitting" );
1302 }
1303
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 )
1307 {
1308 npsd[f - f0] = log10( std::max( measuredPsd[f], tiny ) );
1309 }
1310
1311 if( noiseEstimateStatistic == "minimum" )
1312 {
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;
1317 }
1318
1319 if( noiseEstimateStatistic != "percentile" )
1320 {
1321 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
1322 "Unknown noise-estimation statistic: " + noiseEstimateStatistic );
1323 }
1324
1325 realT pct = static_cast<realT>( 0.25 );
1326 if( modeIndex < 2 )
1327 {
1328 pct = static_cast<realT>( 0.05 );
1329 }
1330
1331 size_t nthIndex = static_cast<size_t>( pct * static_cast<realT>( npsd.size() ) );
1332 if( nthIndex >= npsd.size() )
1333 {
1334 nthIndex = npsd.size() - 1;
1335 }
1336
1337 auto nth = npsd.begin() + nthIndex;
1338 std::nth_element( npsd.begin(), nth, npsd.end() );
1339
1340 noiseFloor = pow( static_cast<realT>( 10 ), *nth );
1341 noisePsd.assign( measuredPsd.size(), noiseFloor );
1342
1343 return mx::error_t::noerror;
1344}
1345
1346template <typename realT>
1348{
1349 std::transform( domain.begin(),
1350 domain.end(),
1351 domain.begin(),
1352 []( unsigned char c )
1353 {
1354 if( c == '_' )
1355 {
1356 return static_cast<char>( '-' );
1357 }
1358
1359 return static_cast<char>( std::tolower( c ) );
1360 } );
1361
1362 return domain;
1363}
1364
1365template <typename realT>
1367{
1368 std::transform( range.begin(),
1369 range.end(),
1370 range.begin(),
1371 []( unsigned char c )
1372 {
1373 if( c == '_' )
1374 {
1375 return static_cast<char>( '-' );
1376 }
1377
1378 return static_cast<char>( std::tolower( c ) );
1379 } );
1380
1381 return range;
1382}
1383
1384template <typename realT>
1386{
1387 std::transform( statistic.begin(),
1388 statistic.end(),
1389 statistic.begin(),
1390 []( unsigned char c )
1391 {
1392 if( c == '_' )
1393 {
1394 return static_cast<char>( '-' );
1395 }
1396
1397 return static_cast<char>( std::tolower( c ) );
1398 } );
1399
1400 return statistic;
1401}
1402
1403template <typename realT>
1405{
1406 std::transform( method.begin(),
1407 method.end(),
1408 method.begin(),
1409 []( unsigned char c )
1410 {
1411 if( c == '_' )
1412 {
1413 return static_cast<char>( '-' );
1414 }
1415
1416 return static_cast<char>( std::tolower( c ) );
1417 } );
1418
1419 return method;
1420}
1421
1422template <typename realT>
1424{
1425 std::transform( mode.begin(),
1426 mode.end(),
1427 mode.begin(),
1428 []( unsigned char c )
1429 {
1430 if( c == '_' )
1431 {
1432 return static_cast<char>( '-' );
1433 }
1434
1435 return static_cast<char>( std::tolower( c ) );
1436 } );
1437
1438 if( mode == "auto" || mode == "automatic" || mode == "auto-crossing" )
1439 {
1440 return "auto-smoothed-crossing";
1441 }
1442
1443 return mode;
1444}
1445
1446template <typename realT>
1447size_t modalPsdProcessor<realT>::firstPositiveFreqIndex( const std::vector<realT> &freq )
1448{
1449 for( size_t n = 0; n < freq.size(); ++n )
1450 {
1451 if( freq[n] > static_cast<realT>( 0 ) )
1452 {
1453 return n;
1454 }
1455 }
1456
1457 return 0;
1458}
1459
1460template <typename realT>
1461mx::error_t modalPsdProcessor<realT>::buildSmoothedProcessPsd( std::vector<realT> &smoothedProcessPsd,
1462 const std::vector<realT> &rawProcessPsd,
1463 const std::vector<realT> &freq,
1464 realT smoothWidthHz )
1465{
1466 if( rawProcessPsd.size() != freq.size() )
1467 {
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" );
1471 }
1472
1473 if( rawProcessPsd.size() < 3 )
1474 {
1475 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr,
1476 "Smoothed disturbance PSD requires at least three bins" );
1477 }
1478
1479 if( smoothWidthHz <= static_cast<realT>( 0 ) )
1480 {
1481 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
1482 "Smoothed disturbance PSD width must be positive" );
1483 }
1484
1485 const size_t firstPositive = firstPositiveFreqIndex( freq );
1486 if( firstPositive >= freq.size() || freq[firstPositive] <= static_cast<realT>( 0 ) )
1487 {
1488 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
1489 "Automatic power-law crossover requires positive frequencies" );
1490 }
1491
1492 if( firstPositive + 1 >= freq.size() )
1493 {
1494 smoothedProcessPsd = rawProcessPsd;
1495 return mx::error_t::noerror;
1496 }
1497
1498 const realT df = freq[firstPositive + 1] - freq[firstPositive];
1499 if( df <= static_cast<realT>( 0 ) )
1500 {
1501 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
1502 "Automatic power-law crossover requires increasing frequency bins" );
1503 }
1504
1505 int win = std::max( 3, static_cast<int>( std::lround( smoothWidthHz / df ) ) );
1506 if( win % 2 == 0 )
1507 {
1508 ++win;
1509 }
1510
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 )
1514 {
1515 logRaw[n] = log10( std::max( rawProcessPsd[n], tiny ) );
1516 }
1517
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 )
1522 {
1523 smoothedProcessPsd[n] = pow( static_cast<realT>( 10 ), logSmooth[n] );
1524 }
1525
1526 return mx::error_t::noerror;
1527}
1528
1529template <typename realT>
1531 const std::vector<realT> &smoothedProcessPsd,
1532 const std::vector<realT> &noisePsd,
1533 const std::vector<realT> &freq,
1534 realT maxFreqFraction )
1535{
1536 if( smoothedProcessPsd.size() != noisePsd.size() || smoothedProcessPsd.size() != freq.size() )
1537 {
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" );
1541 }
1542
1543 if( smoothedProcessPsd.size() < 3 )
1544 {
1545 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr,
1546 "Automatic power-law crossover requires at least three bins" );
1547 }
1548
1549 const size_t firstPositive = firstPositiveFreqIndex( freq );
1550 if( firstPositive >= freq.size() || freq[firstPositive] <= static_cast<realT>( 0 ) )
1551 {
1552 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
1553 "Automatic power-law crossover requires positive frequencies" );
1554 }
1555
1556 size_t lastSearch = freq.size() - 1;
1557 if( maxFreqFraction > static_cast<realT>( 0 ) && maxFreqFraction < static_cast<realT>( 1 ) )
1558 {
1559 realT maxSearchFreq = maxFreqFraction * freq.back();
1560 auto upper =
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 ) )
1563 {
1564 lastSearch = firstPositive;
1565 }
1566 else
1567 {
1568 lastSearch = static_cast<size_t>( ( upper - freq.begin() ) - 1 );
1569 }
1570 }
1571
1572 if( lastSearch <= firstPositive )
1573 {
1574 crossoverFreq = freq[firstPositive];
1575 return mx::error_t::noerror;
1576 }
1577
1578 bool foundCrossing = false;
1579 realT lastCrossingFreq = freq[firstPositive];
1580 for( size_t n = firstPositive; n < lastSearch; ++n )
1581 {
1582 realT d0 = smoothedProcessPsd[n] - noisePsd[n];
1583 realT d1 = smoothedProcessPsd[n + 1] - noisePsd[n + 1];
1584
1585 if( d0 == static_cast<realT>( 0 ) && d1 == static_cast<realT>( 0 ) )
1586 {
1587 lastCrossingFreq = freq[n + 1];
1588 foundCrossing = true;
1589 continue;
1590 }
1591
1592 if( ( d0 <= static_cast<realT>( 0 ) && d1 >= static_cast<realT>( 0 ) ) ||
1593 ( d0 >= static_cast<realT>( 0 ) && d1 <= static_cast<realT>( 0 ) ) )
1594 {
1595 realT alpha = static_cast<realT>( 0 );
1596 if( d0 != d1 )
1597 {
1598 alpha = d0 / ( d0 - d1 );
1599 }
1600
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;
1604 }
1605 }
1606
1607 if( foundCrossing )
1608 {
1609 crossoverFreq = lastCrossingFreq;
1610 return mx::error_t::noerror;
1611 }
1612
1613 size_t minimumIndex = firstPositive;
1614 realT minimumPsd = smoothedProcessPsd[firstPositive];
1615 for( size_t n = firstPositive + 1; n <= lastSearch; ++n )
1616 {
1617 if( smoothedProcessPsd[n] <= minimumPsd )
1618 {
1619 minimumPsd = smoothedProcessPsd[n];
1620 minimumIndex = n;
1621 }
1622 }
1623
1624 crossoverFreq = freq[minimumIndex];
1625 return mx::error_t::noerror;
1626}
1627
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 )
1637{
1638 powerLawCrossoverMode = normalizePowerLawCrossoverMode( powerLawCrossoverMode );
1639 if( powerLawCrossoverMode != "auto-smoothed-crossing" )
1640 {
1641 return mx::error_t::noerror;
1642 }
1643
1644 static_cast<void>( rawProcessPsd );
1645
1646 realT crossoverFreq = static_cast<realT>( 0 );
1647 mx::error_t errc =
1648 findAutoPowerLawCrossoverFreq( crossoverFreq, smoothedProcessPsd, noisePsd, freq, powerLawAutoMaxFreqFraction );
1649 if( !!errc )
1650 {
1651 return errc;
1652 }
1653
1654 auto upper = std::lower_bound( freq.begin(), freq.end(), crossoverFreq );
1655 if( upper == freq.end() )
1656 {
1657 crossoverFreq = freq.back();
1658 }
1659 else
1660 {
1661 crossoverFreq = *upper;
1662 }
1663
1664 powerLawMatchFreq = crossoverFreq;
1665 powerLawOnlyAboveFreq = crossoverFreq;
1666 return mx::error_t::noerror;
1667}
1668
1669template <typename realT>
1670realT modalPsdProcessor<realT>::resolvePowerLawNormFreq( const std::vector<realT> &freq, realT requestedNormFreq )
1671{
1672 if( requestedNormFreq > static_cast<realT>( 0 ) )
1673 {
1674 return requestedNormFreq;
1675 }
1676
1677 size_t refIndex = firstPositiveFreqIndex( freq );
1678 if( refIndex < freq.size() && freq[refIndex] > static_cast<realT>( 0 ) )
1679 {
1680 return freq[refIndex];
1681 }
1682
1683 return static_cast<realT>( 1 );
1684}
1685
1686template <typename realT>
1688 realT extrapolation, const std::vector<realT> &freq, size_t index, realT powerLawIndex, realT powerLawNormFreq )
1689{
1690 const realT refFreq = resolvePowerLawNormFreq( freq, powerLawNormFreq );
1691 const realT useFreq = freq[index] > static_cast<realT>( 0 ) ? freq[index] : refFreq;
1692
1693 return extrapolation * pow( refFreq / useFreq, powerLawIndex );
1694}
1695
1696template <typename realT>
1697realT modalPsdProcessor<realT>::interpolatePsdAtFreq( const std::vector<realT> &psd,
1698 const std::vector<realT> &freq,
1699 realT targetFreq )
1700{
1701 if( psd.empty() || psd.size() != freq.size() )
1702 {
1703 return std::numeric_limits<realT>::quiet_NaN();
1704 }
1705
1706 auto upper = std::lower_bound( freq.begin(), freq.end(), targetFreq );
1707 if( upper == freq.begin() )
1708 {
1709 return psd.front();
1710 }
1711
1712 if( upper == freq.end() )
1713 {
1714 return psd.back();
1715 }
1716
1717 size_t hi = upper - freq.begin();
1718 if( *upper == targetFreq )
1719 {
1720 return psd[hi];
1721 }
1722
1723 size_t lo = hi - 1;
1724 if( freq[hi] == freq[lo] )
1725 {
1726 return psd[lo];
1727 }
1728
1729 realT alpha = ( targetFreq - freq[lo] ) / ( freq[hi] - freq[lo] );
1730 return ( static_cast<realT>( 1 ) - alpha ) * psd[lo] + alpha * psd[hi];
1731}
1732
1733template <typename realT>
1734mx::error_t modalPsdProcessor<realT>::matchPowerLawAtFreq( realT &extrapolation,
1735 const std::vector<realT> &anchorProcessPsd,
1736 const std::vector<realT> &freq,
1737 realT powerLawIndex,
1738 realT powerLawNormFreq,
1739 realT powerLawMatchFreq,
1740 realT powerLawMatchFallbackWindowHz )
1741{
1742 if( powerLawMatchFreq <= static_cast<realT>( 0 ) )
1743 {
1744 return mx::error_t::noerror;
1745 }
1746
1747 if( anchorProcessPsd.size() != freq.size() || anchorProcessPsd.empty() )
1748 {
1749 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr,
1750 "Anchor disturbance PSD and frequency grid must have the same size" );
1751 }
1752
1753 size_t firstPositive = firstPositiveFreqIndex( freq );
1754 if( firstPositive >= freq.size() || freq[firstPositive] <= static_cast<realT>( 0 ) )
1755 {
1756 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg, "Frequency grid must contain positive bins" );
1757 }
1758
1759 if( powerLawMatchFreq < freq[firstPositive] || powerLawMatchFreq > freq.back() )
1760 {
1761 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
1762 "Power-law match frequency is outside the sampled frequency range" );
1763 }
1764
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 ) )
1769 {
1770 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
1771 "Could not interpolate the disturbance PSD at the match frequency" );
1772 }
1773
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 )
1780 {
1781 if( anchorProcessPsd[n] > tiny )
1782 {
1783 localPositivePsd.push_back( anchorProcessPsd[n] );
1784 }
1785 }
1786
1787 if( !localPositivePsd.empty() )
1788 {
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 )
1793 {
1794 auto lowerIt = std::max_element( localPositivePsd.begin(), localPositivePsd.begin() + mid );
1795 localMedian = static_cast<realT>( 0.5 ) * ( localMedian + *lowerIt );
1796 }
1797
1798 if( matchPsd <= tiny || matchPsd < static_cast<realT>( 0.5 ) * localMedian )
1799 {
1800 matchPsd = localMedian;
1801 }
1802 }
1803
1804 extrapolation = std::max( matchPsd, tiny ) * pow( powerLawMatchFreq / refFreq, powerLawIndex );
1805 return mx::error_t::noerror;
1806}
1807
1808template <typename realT>
1810{
1811 return fwhm / ( static_cast<realT>( 2 ) *
1812 sqrt( pow( static_cast<realT>( 2 ), static_cast<realT>( 1 ) / beta ) - static_cast<realT>( 1 ) ) );
1813}
1814
1815template <typename realT>
1816realT modalPsdProcessor<realT>::moffatValueFromFwhm( realT radius, realT peakHeight, realT fwhm, realT beta )
1817{
1818 const realT alpha = moffatAlphaFromFwhm( fwhm, beta );
1819 return mx::math::func::moffat<realT>( radius,
1820 static_cast<realT>( 0 ),
1821 peakHeight,
1822 static_cast<realT>( 0 ),
1823 alpha,
1824 beta );
1825}
1826
1827template <typename realT>
1829 realT peakHeight, realT fwhm, realT radius, realT target, realT betaFloor )
1830{
1831 if( peakHeight <= static_cast<realT>( 0 ) || fwhm <= static_cast<realT>( 0 ) || radius <= static_cast<realT>( 0 ) ||
1832 target <= static_cast<realT>( 0 ) )
1833 {
1834 return betaFloor;
1835 }
1836
1837 realT low = std::max( betaFloor, static_cast<realT>( 1.0e-3 ) );
1838 if( moffatValueFromFwhm( radius, peakHeight, fwhm, low ) <= target )
1839 {
1840 return low;
1841 }
1842
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 )
1845 {
1846 high *= static_cast<realT>( 2 );
1847 }
1848
1849 if( moffatValueFromFwhm( radius, peakHeight, fwhm, high ) > target )
1850 {
1851 return high;
1852 }
1853
1854 for( int n = 0; n < 60; ++n )
1855 {
1856 realT mid = static_cast<realT>( 0.5 ) * ( low + high );
1857 if( moffatValueFromFwhm( radius, peakHeight, fwhm, mid ) > target )
1858 {
1859 low = mid;
1860 }
1861 else
1862 {
1863 high = mid;
1864 }
1865 }
1866
1867 return high;
1868}
1869
1870template <typename realT>
1871realT modalPsdProcessor<realT>::interpolateCrossing( realT x0, realT y0, realT x1, realT y1, realT target )
1872{
1873 if( y1 == y0 )
1874 {
1875 return static_cast<realT>( 0.5 ) * ( x0 + x1 );
1876 }
1877
1878 realT alpha = ( target - y0 ) / ( y1 - y0 );
1879 if( alpha < static_cast<realT>( 0 ) )
1880 {
1881 alpha = static_cast<realT>( 0 );
1882 }
1883 else if( alpha > static_cast<realT>( 1 ) )
1884 {
1885 alpha = static_cast<realT>( 1 );
1886 }
1887
1888 return x0 + alpha * ( x1 - x0 );
1889}
1890
1891template <typename realT>
1893 size_t &nBinsUsed,
1894 const std::vector<realT> &rawProcessPsd,
1895 const std::vector<realT> &freq,
1896 realT fitMinFreqHz,
1897 realT fitMaxFreqHz,
1898 realT fitBinWidthHz,
1899 realT includeMatchFreqHz )
1900{
1901 const realT originalPowerLawIndex = powerLawIndex;
1902
1903 if( rawProcessPsd.size() != freq.size() )
1904 {
1905 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr,
1906 "Raw disturbance PSD and frequency grid must have the same size" );
1907 }
1908
1909 if( fitMinFreqHz <= static_cast<realT>( 0 ) || fitMaxFreqHz <= fitMinFreqHz ||
1910 fitBinWidthHz <= static_cast<realT>( 0 ) )
1911 {
1912 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
1913 "Invalid power-law exponent fit range or bin width" );
1914 }
1915
1916 if( fitMinFreqHz >= freq.back() )
1917 {
1918 nBinsUsed = 0;
1919 powerLawIndex = std::max( originalPowerLawIndex, static_cast<realT>( 0 ) );
1920 return mx::error_t::noerror;
1921 }
1922
1923 const realT tiny = std::numeric_limits<realT>::min();
1924 std::vector<realT> x;
1925 std::vector<realT> y;
1926
1927 size_t idx = std::lower_bound( freq.begin(), freq.end(), fitMinFreqHz ) - freq.begin();
1928 for( realT binStart = fitMinFreqHz; binStart < fitMaxFreqHz; binStart += fitBinWidthHz )
1929 {
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 ) )
1933 {
1934 continue;
1935 }
1936
1937 std::vector<realT> logPsd;
1938 while( idx < freq.size() && freq[idx] < binEnd )
1939 {
1940 if( rawProcessPsd[idx] > tiny )
1941 {
1942 logPsd.push_back( log10( rawProcessPsd[idx] ) );
1943 }
1944
1945 ++idx;
1946 }
1947
1948 if( logPsd.empty() )
1949 {
1950 continue;
1951 }
1952
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 )
1957 {
1958 auto lowerIt = std::max_element( logPsd.begin(), logPsd.begin() + mid );
1959 medianLogPsd = static_cast<realT>( 0.5 ) * ( medianLogPsd + *lowerIt );
1960 }
1961
1962 x.push_back( log10( binCenter ) );
1963 y.push_back( medianLogPsd );
1964 }
1965
1966 if( includeMatchFreqHz > static_cast<realT>( 0 ) &&
1967 ( includeMatchFreqHz < fitMinFreqHz || includeMatchFreqHz >= fitMaxFreqHz ) )
1968 {
1969 realT matchPsd = interpolatePsdAtFreq( rawProcessPsd, freq, includeMatchFreqHz );
1970 if( std::isfinite( matchPsd ) && matchPsd > tiny )
1971 {
1972 x.push_back( log10( includeMatchFreqHz ) );
1973 y.push_back( log10( matchPsd ) );
1974 }
1975 }
1976
1977 nBinsUsed = x.size();
1978 if( nBinsUsed < 2 )
1979 {
1980 nBinsUsed = 0;
1981 powerLawIndex = std::max( originalPowerLawIndex, static_cast<realT>( 0 ) );
1982 return mx::error_t::noerror;
1983 }
1984
1985 realT meanX = 0;
1986 realT meanY = 0;
1987 for( size_t n = 0; n < nBinsUsed; ++n )
1988 {
1989 meanX += x[n];
1990 meanY += y[n];
1991 }
1992
1993 meanX /= static_cast<realT>( nBinsUsed );
1994 meanY /= static_cast<realT>( nBinsUsed );
1995
1996 realT varX = 0;
1997 realT covXY = 0;
1998 for( size_t n = 0; n < nBinsUsed; ++n )
1999 {
2000 realT dx = x[n] - meanX;
2001 realT dy = y[n] - meanY;
2002 varX += dx * dx;
2003 covXY += dx * dy;
2004 }
2005
2006 if( varX <= static_cast<realT>( 0 ) )
2007 {
2008 nBinsUsed = 0;
2009 powerLawIndex = std::max( originalPowerLawIndex, static_cast<realT>( 0 ) );
2010 return mx::error_t::noerror;
2011 }
2012
2013 powerLawIndex = -covXY / varX;
2014 if( !std::isfinite( powerLawIndex ) )
2015 {
2016 nBinsUsed = 0;
2017 powerLawIndex = std::max( originalPowerLawIndex, static_cast<realT>( 0 ) );
2018 return mx::error_t::noerror;
2019 }
2020
2021 if( powerLawIndex < static_cast<realT>( 0 ) )
2022 {
2023 powerLawIndex = static_cast<realT>( 0 );
2024 }
2025
2026 return mx::error_t::noerror;
2027}
2028
2029template <typename realT>
2030mx::error_t modalPsdProcessor<realT>::estimatePowerLawContinuum( std::vector<realT> &continuumPsd,
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 )
2048{
2049 if( rawProcessPsd.size() != noisePsd.size() || rawProcessPsd.size() != freq.size() ||
2050 anchorProcessPsd.size() != rawProcessPsd.size() )
2051 {
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" );
2055 }
2056
2057 if( rawProcessPsd.size() < 2 )
2058 {
2059 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr, "At least two frequency bins are required" );
2060 }
2061
2062 if( powerLawIndex < static_cast<realT>( 0 ) && !fitPowerLawIndex )
2063 {
2064 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg, "Power-law index must be non-negative" );
2065 }
2066
2067 const realT tiny = std::numeric_limits<realT>::min();
2068 size_t localFitBinsUsed = 0;
2069 realT localPowerLawIndex = powerLawIndex;
2070 if( fitPowerLawIndex )
2071 {
2072 mx::error_t errc = fitPowerLawIndexFromBinnedMedians( localPowerLawIndex,
2073 localFitBinsUsed,
2074 rawProcessPsd,
2075 freq,
2076 powerLawFitMinFreqHz,
2077 powerLawFitMaxFreqHz,
2078 powerLawFitBinWidthHz,
2079 powerLawFitIncludesMatchPoint ? powerLawMatchFreq
2080 : static_cast<realT>( 0 ) );
2081 if( !!errc )
2082 {
2083 localPowerLawIndex = std::max( powerLawIndex, static_cast<realT>( 0 ) );
2084 localFitBinsUsed = 0;
2085 }
2086 }
2087
2088 const size_t refIndex = firstPositiveFreqIndex( freq );
2089 const realT refFreq = resolvePowerLawNormFreq( freq, powerLawNormFreq );
2090 extrapolation = 0;
2091 anchorIndex = std::min( refIndex, freq.size() - 1 );
2092 int nExtrap = 0;
2093
2094 size_t fMax = static_cast<size_t>( static_cast<realT>( 0.05 ) * static_cast<realT>( freq.size() ) );
2095 if( fMax < 2 )
2096 {
2097 fMax = std::min<size_t>( freq.size(), 2 );
2098 }
2099
2100 for( size_t f = 1; f < fMax; ++f )
2101 {
2102 if( anchorProcessPsd[f] <= static_cast<realT>( 0.1 ) * noisePsd[f] )
2103 {
2104 continue;
2105 }
2106
2107 extrapolation += log10( anchorProcessPsd[f] * pow( freq[f] / refFreq, localPowerLawIndex ) );
2108 anchorIndex = f;
2109 ++nExtrap;
2110 }
2111
2112 if( nExtrap > 0 )
2113 {
2114 extrapolation = pow( static_cast<realT>( 10 ), extrapolation / static_cast<realT>( nExtrap ) );
2115 }
2116 else
2117 {
2118 const realT useFreq = freq[refIndex] > static_cast<realT>( 0 ) ? freq[refIndex] : refFreq;
2119 extrapolation =
2120 std::max( anchorProcessPsd[refIndex] * static_cast<realT>( pow( useFreq / refFreq, localPowerLawIndex ) ),
2121 tiny );
2122 }
2123
2124 mx::error_t errc = matchPowerLawAtFreq( extrapolation,
2125 anchorProcessPsd,
2126 freq,
2127 localPowerLawIndex,
2128 refFreq,
2129 powerLawMatchFreq,
2130 powerLawMatchFallbackWindowHz );
2131 if( !!errc )
2132 {
2133 return errc;
2134 }
2135
2136 errc = buildPowerLawContinuum( continuumPsd, extrapolation, freq, localPowerLawIndex, refFreq );
2137 if( !!errc )
2138 {
2139 return errc;
2140 }
2141
2142 if( usedPowerLawIndex )
2143 {
2144 *usedPowerLawIndex = localPowerLawIndex;
2145 }
2146
2147 if( fitBinsUsed )
2148 {
2149 *fitBinsUsed = localFitBinsUsed;
2150 }
2151
2152 return mx::error_t::noerror;
2153}
2154
2155template <typename realT>
2156mx::error_t modalPsdProcessor<realT>::buildPowerLawContinuum( std::vector<realT> &continuumPsd,
2157 realT extrapolation,
2158 const std::vector<realT> &freq,
2159 realT powerLawIndex,
2160 realT powerLawNormFreq )
2161{
2162 const realT tiny = std::numeric_limits<realT>::min();
2163
2164 continuumPsd.resize( freq.size() );
2165 for( size_t n = 0; n < freq.size(); ++n )
2166 {
2167 continuumPsd[n] =
2168 std::max( powerLawContinuum( extrapolation, freq, n, powerLawIndex, powerLawNormFreq ), tiny );
2169 }
2170
2171 return mx::error_t::noerror;
2172}
2173
2174template <typename realT>
2175mx::error_t modalPsdProcessor<realT>::blendContinuumAtAnchor( std::vector<realT> &processPsd,
2176 const std::vector<realT> &rawProcessPsd,
2177 const std::vector<realT> &continuumPsd,
2178 size_t anchorIndex,
2179 int blendBins )
2180{
2181 if( rawProcessPsd.size() != continuumPsd.size() )
2182 {
2183 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr,
2184 "Raw disturbance PSD and continuum PSD must have the same size" );
2185 }
2186
2187 if( rawProcessPsd.empty() )
2188 {
2189 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr, "At least one frequency bin is required" );
2190 }
2191
2192 if( blendBins < 1 )
2193 {
2194 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg, "Blend width must be positive" );
2195 }
2196
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 );
2200
2201 processPsd.resize( rawProcessPsd.size() );
2202 for( size_t n = 0; n < processPsd.size(); ++n )
2203 {
2204 if( n <= anchorIndex )
2205 {
2206 processPsd[n] = std::max( rawProcessPsd[n], tiny );
2207 continue;
2208 }
2209
2210 if( n >= blendEnd || blendEnd == anchorIndex )
2211 {
2212 processPsd[n] = std::max( continuumPsd[n], tiny );
2213 continue;
2214 }
2215
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 );
2220 }
2221
2222 return mx::error_t::noerror;
2223}
2224
2225template <typename realT>
2226mx::error_t modalPsdProcessor<realT>::identifyMoffatPeaks( std::vector<identifiedPeak1D> &peaks,
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 )
2234{
2235 if( rawProcessPsd.size() != continuumPsd.size() || rawProcessPsd.size() != freq.size() )
2236 {
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" );
2240 }
2241
2242 if( rawProcessPsd.size() < 3 )
2243 {
2244 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr, "At least three frequency bins are required" );
2245 }
2246
2247 if( peakDetectWidthHz <= static_cast<realT>( 0 ) )
2248 {
2249 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg, "Peak-detection width must be positive" );
2250 }
2251
2252 if( peakDetectFactor <= static_cast<realT>( 1 ) )
2253 {
2254 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg, "Peak-detection factor must exceed unity" );
2255 }
2256
2257 if( peakDetectBroadFactor <= static_cast<realT>( 1 ) )
2258 {
2259 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
2260 "Broad peak-detection factor must exceed unity" );
2261 }
2262
2263 if( peakDetectMinWidthLog <= static_cast<realT>( 0 ) )
2264 {
2265 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
2266 "Broad peak minimum log-width must be positive" );
2267 }
2268
2269 const realT df = freq[1] - freq[0];
2270 if( df <= static_cast<realT>( 0 ) )
2271 {
2272 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg, "Frequency spacing must be positive" );
2273 }
2274
2275 int win = std::max( 3, static_cast<int>( std::lround( peakDetectWidthHz / df ) ) );
2276 if( win % 2 == 0 )
2277 {
2278 ++win;
2279 }
2280
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 )
2284 {
2285 logRaw[n] = log10( std::max( rawProcessPsd[n], tiny ) );
2286 }
2287
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 )
2292 {
2293 smoothPsd[n] = pow( static_cast<realT>( 10 ), logSmooth[n] );
2294 }
2295
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 )
2299 {
2300 if( rawProcessPsd[n] > peakDetectFactor * smoothPsd[n] )
2301 {
2302 strongMask[n] = 1;
2303 candidateMask[n] = 1;
2304 }
2305
2306 if( rawProcessPsd[n] > peakDetectBroadFactor * smoothPsd[n] )
2307 {
2308 candidateMask[n] = 1;
2309 }
2310 }
2311
2312 peaks.clear();
2313 std::vector<unsigned char> peakHasStrong;
2314 bool inPeak = false;
2315 bool currentHasStrong = false;
2316 identifiedPeak1D currentPeak;
2317
2318 for( size_t n = 1; n < rawProcessPsd.size(); ++n )
2319 {
2320 if( candidateMask[n] )
2321 {
2322 if( !inPeak )
2323 {
2324 inPeak = true;
2325 currentHasStrong = ( strongMask[n] != 0 );
2326 currentPeak = identifiedPeak1D{};
2327 currentPeak.m_start = n;
2328 currentPeak.m_end = n;
2329 currentPeak.m_peakIndex = n;
2330 }
2331 else
2332 {
2333 currentPeak.m_end = n;
2334 currentHasStrong = currentHasStrong || ( strongMask[n] != 0 );
2335 if( rawProcessPsd[n] > rawProcessPsd[currentPeak.m_peakIndex] )
2336 {
2337 currentPeak.m_peakIndex = n;
2338 }
2339 }
2340 }
2341 else if( inPeak )
2342 {
2343 inPeak = false;
2344 peaks.push_back( currentPeak );
2345 peakHasStrong.push_back( currentHasStrong ? 1 : 0 );
2346 }
2347 }
2348
2349 if( inPeak )
2350 {
2351 peaks.push_back( currentPeak );
2352 peakHasStrong.push_back( currentHasStrong ? 1 : 0 );
2353 }
2354
2355 std::vector<identifiedPeak1D> validPeaks;
2356 validPeaks.reserve( peaks.size() );
2357
2358 for( size_t p = 0; p < peaks.size(); ++p )
2359 {
2360 identifiedPeak1D peak = peaks[p];
2361 peak.m_centerFreq = freq[peak.m_peakIndex];
2362 peak.m_peakHeight = rawProcessPsd[peak.m_peakIndex] - continuumPsd[peak.m_peakIndex];
2363 if( peak.m_peakHeight <= static_cast<realT>( 0 ) )
2364 {
2365 continue;
2366 }
2367
2368 const realT halfLevel = continuumPsd[peak.m_peakIndex] + static_cast<realT>( 0.5 ) * peak.m_peakHeight;
2369 const realT defaultFwhm = std::max( df, static_cast<realT>( peak.m_end - peak.m_start + 1 ) * df );
2370
2371 realT leftCross = peak.m_centerFreq - static_cast<realT>( 0.5 ) * defaultFwhm;
2372 for( size_t n = peak.m_peakIndex; n > 0; --n )
2373 {
2374 if( rawProcessPsd[n - 1] <= halfLevel )
2375 {
2376 leftCross =
2377 interpolateCrossing( freq[n - 1], rawProcessPsd[n - 1], freq[n], rawProcessPsd[n], halfLevel );
2378 break;
2379 }
2380 }
2381
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 )
2384 {
2385 if( rawProcessPsd[n + 1] <= halfLevel )
2386 {
2387 rightCross =
2388 interpolateCrossing( freq[n], rawProcessPsd[n], freq[n + 1], rawProcessPsd[n + 1], halfLevel );
2389 break;
2390 }
2391 }
2392
2393 peak.m_fwhm = rightCross - leftCross;
2394 if( peak.m_fwhm <= static_cast<realT>( 0 ) )
2395 {
2396 peak.m_fwhm = defaultFwhm;
2397 }
2398
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 );
2403
2404 if( !peakHasStrong[p] && logWidth < peakDetectMinWidthLog )
2405 {
2406 continue;
2407 }
2408
2409 validPeaks.push_back( peak );
2410 }
2411
2412 peaks.swap( validPeaks );
2413 return mx::error_t::noerror;
2414}
2415
2416template <typename realT>
2417mx::error_t modalPsdProcessor<realT>::addClippedMoffatPeakExcess( std::vector<realT> &peakModel,
2418 const identifiedPeak1D &peak,
2419 const std::vector<realT> &sourcePsd,
2420 const std::vector<realT> &continuumPsd,
2421 const std::vector<realT> &freq,
2422 realT peakMoffatBeta )
2423{
2424 if( peakModel.size() != sourcePsd.size() || peakModel.size() != continuumPsd.size() ||
2425 peakModel.size() != freq.size() )
2426 {
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" );
2430 }
2431
2432 if( peak.m_peakIndex >= peakModel.size() || peak.m_fwhm <= static_cast<realT>( 0 ) )
2433 {
2434 return mx::error_t::noerror;
2435 }
2436
2437 realT peakBeta = peakMoffatBeta;
2438
2439 if( peak.m_start > 0 && peak.m_start <= peak.m_peakIndex )
2440 {
2441 const size_t edge = peak.m_start - 1;
2442 const realT radius = peak.m_centerFreq - freq[edge];
2443 peakBeta = std::max(
2444 peakBeta,
2445 fitMoffatBetaToBoundary( peak.m_peakHeight, peak.m_fwhm, radius, continuumPsd[edge], peakMoffatBeta ) );
2446 }
2447
2448 if( peak.m_end + 1 < peakModel.size() && peak.m_end >= peak.m_peakIndex )
2449 {
2450 const size_t edge = peak.m_end + 1;
2451 const realT radius = freq[edge] - peak.m_centerFreq;
2452 peakBeta = std::max(
2453 peakBeta,
2454 fitMoffatBetaToBoundary( peak.m_peakHeight, peak.m_fwhm, radius, continuumPsd[edge], peakMoffatBeta ) );
2455 }
2456
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 ),
2460 peak.m_peakHeight,
2461 peak.m_centerFreq,
2462 alpha,
2463 peakBeta );
2464 if( peakAtCenter <= continuumPsd[peak.m_peakIndex] )
2465 {
2466 return mx::error_t::noerror;
2467 }
2468
2469 size_t left = peak.m_peakIndex;
2470 while( left > 0 && sourcePsd[left - 1] > continuumPsd[left - 1] )
2471 {
2472 --left;
2473 }
2474
2475 size_t right = peak.m_peakIndex;
2476 while( right + 1 < peakModel.size() && sourcePsd[right + 1] > continuumPsd[right + 1] )
2477 {
2478 ++right;
2479 }
2480
2481 for( size_t n = left; n <= right; ++n )
2482 {
2483 realT peakValue = mx::math::func::moffat<realT>( freq[n],
2484 static_cast<realT>( 0 ),
2485 peak.m_peakHeight,
2486 peak.m_centerFreq,
2487 alpha,
2488 peakBeta );
2489 realT measuredExcess = std::max( sourcePsd[n] - continuumPsd[n], static_cast<realT>( 0 ) );
2490 if( measuredExcess > static_cast<realT>( 0 ) )
2491 {
2492 peakModel[n] += std::min( peakValue, measuredExcess );
2493 }
2494 }
2495
2496 return mx::error_t::noerror;
2497}
2498
2499template <typename realT>
2500mx::error_t modalPsdProcessor<realT>::identifyMoffatPeaksMultiPass( std::vector<identifiedPeak1D> &peaks,
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 )
2510{
2511 if( peakDetectPasses < 1 )
2512 {
2513 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg, "Peak-detection passes must be positive" );
2514 }
2515
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 )
2519 {
2520 workingExcess[n] = std::max( rawProcessPsd[n] - continuumPsd[n], static_cast<realT>( 0 ) );
2521 }
2522
2523 peaks.clear();
2524
2525 for( int pass = 0; pass < peakDetectPasses; ++pass )
2526 {
2527 std::vector<realT> workingPsd( rawProcessPsd.size() );
2528 for( size_t n = 0; n < workingPsd.size(); ++n )
2529 {
2530 workingPsd[n] = std::max( continuumPsd[n] + workingExcess[n], tiny );
2531 }
2532
2533 std::vector<identifiedPeak1D> passPeaks;
2534 mx::error_t errc = identifyMoffatPeaks( passPeaks,
2535 workingPsd,
2536 continuumPsd,
2537 freq,
2538 peakDetectWidthHz,
2539 peakDetectFactor,
2540 peakDetectBroadFactor,
2541 peakDetectMinWidthLog );
2542 if( !!errc )
2543 {
2544 return errc;
2545 }
2546
2547 if( passPeaks.empty() )
2548 {
2549 break;
2550 }
2551
2552 std::vector<realT> passModel( rawProcessPsd.size(), static_cast<realT>( 0 ) );
2553 for( size_t p = 0; p < passPeaks.size(); ++p )
2554 {
2555 errc =
2556 addClippedMoffatPeakExcess( passModel, passPeaks[p], workingPsd, continuumPsd, freq, peakMoffatBeta );
2557 if( !!errc )
2558 {
2559 return errc;
2560 }
2561 }
2562
2563 bool subtracted = false;
2564 for( size_t n = 0; n < workingExcess.size(); ++n )
2565 {
2566 realT newExcess = std::max( workingExcess[n] - passModel[n], static_cast<realT>( 0 ) );
2567 if( newExcess < workingExcess[n] )
2568 {
2569 subtracted = true;
2570 }
2571 workingExcess[n] = newExcess;
2572 }
2573
2574 peaks.insert( peaks.end(), passPeaks.begin(), passPeaks.end() );
2575 if( !subtracted )
2576 {
2577 break;
2578 }
2579 }
2580
2581 std::sort( peaks.begin(),
2582 peaks.end(),
2583 []( const identifiedPeak1D &a, const identifiedPeak1D &b ) { return a.m_peakIndex < b.m_peakIndex; } );
2584
2585 auto newEnd = std::unique( peaks.begin(),
2586 peaks.end(),
2587 []( const identifiedPeak1D &a, const identifiedPeak1D &b )
2588 { return a.m_peakIndex == b.m_peakIndex; } );
2589 peaks.erase( newEnd, peaks.end() );
2590
2591 return mx::error_t::noerror;
2592}
2593
2594template <typename realT>
2595mx::error_t modalPsdProcessor<realT>::buildMoffatProcessFromContinuum( std::vector<realT> &processPsd,
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,
2602 size_t anchorIndex,
2603 const processModelConfig &config )
2604{
2605 if( rawProcessPsd.size() != noisePsd.size() || rawProcessPsd.size() != continuumPsd.size() ||
2606 rawProcessPsd.size() != freq.size() )
2607 {
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" );
2611 }
2612
2613 const realT tiny = std::numeric_limits<realT>::min();
2614
2615 mx::error_t errc = identifyMoffatPeaksMultiPass( peaks,
2616 rawProcessPsd,
2617 continuumPsd,
2618 freq,
2619 config.m_peakDetectWidthHz,
2620 config.m_peakDetectFactor,
2623 config.m_peakDetectPasses,
2624 config.m_peakMoffatBeta );
2625 if( !!errc )
2626 {
2627 return errc;
2628 }
2629
2630 std::vector<realT> peakModel( rawProcessPsd.size(), static_cast<realT>( 0 ) );
2631 for( size_t p = 0; p < peaks.size(); ++p )
2632 {
2633 errc = addClippedMoffatPeakExcess( peakModel,
2634 peaks[p],
2635 rawProcessPsd,
2636 continuumPsd,
2637 freq,
2638 config.m_peakMoffatBeta );
2639 if( !!errc )
2640 {
2641 return errc;
2642 }
2643 }
2644
2645 std::vector<realT> highFreqModel( rawProcessPsd.size() );
2646 for( size_t n = 0; n < highFreqModel.size(); ++n )
2647 {
2648 if( config.m_powerLawOnlyAboveFreq > static_cast<realT>( 0 ) && freq[n] >= config.m_powerLawOnlyAboveFreq )
2649 {
2650 highFreqModel[n] = std::max( continuumPsd[n], tiny );
2651 continue;
2652 }
2653
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 );
2656 }
2657
2658 std::vector<realT> extrapolatedPsd;
2659 errc = blendContinuumAtAnchor( extrapolatedPsd,
2660 rawProcessPsd,
2661 highFreqModel,
2662 anchorIndex,
2663 config.m_powerLawBlendBins );
2664 if( !!errc )
2665 {
2666 return errc;
2667 }
2668
2669 processPsd.resize( rawProcessPsd.size() );
2670 repairMask.assign( rawProcessPsd.size(), 0 );
2671 size_t blendEnd =
2672 std::min( anchorIndex + static_cast<size_t>( config.m_powerLawBlendBins ), rawProcessPsd.size() - 1 );
2673 for( size_t n = 0; n <= blendEnd; ++n )
2674 {
2675 repairMask[n] = 1;
2676 }
2677
2678 for( size_t p = 0; p < peaks.size(); ++p )
2679 {
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 )
2683 {
2684 repairMask[n] = 1;
2685 }
2686 }
2687
2688 if( config.m_powerLawOnlyAboveFreq > static_cast<realT>( 0 ) )
2689 {
2690 for( size_t n = 0; n < repairMask.size(); ++n )
2691 {
2692 if( freq[n] >= config.m_powerLawOnlyAboveFreq )
2693 {
2694 repairMask[n] = 0;
2695 }
2696 }
2697 }
2698
2699 for( size_t n = 0; n < processPsd.size(); ++n )
2700 {
2701 if( config.m_powerLawOnlyAboveFreq > static_cast<realT>( 0 ) && freq[n] >= config.m_powerLawOnlyAboveFreq )
2702 {
2703 processPsd[n] = std::max( continuumPsd[n], tiny );
2704 continue;
2705 }
2706
2707 if( rawProcessPsd[n] > noisePsd[n] )
2708 {
2709 processPsd[n] = std::max( rawProcessPsd[n], tiny );
2710 }
2711 else
2712 {
2713 processPsd[n] = std::max( extrapolatedPsd[n], tiny );
2714 }
2715 }
2716
2717 return mx::error_t::noerror;
2718}
2719
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,
2727 size_t anchorIndex,
2728 const processModelConfig &config )
2729{
2730 if( rawProcessPsd.size() != noisePsd.size() || rawProcessPsd.size() != continuumPsd.size() ||
2731 rawProcessPsd.size() != freq.size() )
2732 {
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" );
2736 }
2737
2738 const realT tiny = std::numeric_limits<realT>::min();
2739 std::vector<realT> extrapolatedPsd;
2740 bool hardAutoHandoff =
2741 normalizePowerLawCrossoverMode( config.m_powerLawCrossoverMode ) == "auto-smoothed-crossing" &&
2742 config.m_powerLawMatchFreq > static_cast<realT>( 0 );
2743 if( hardAutoHandoff )
2744 {
2745 extrapolatedPsd = continuumPsd;
2746 }
2747 else
2748 {
2749 mx::error_t errc = blendContinuumAtAnchor( extrapolatedPsd,
2750 rawProcessPsd,
2751 continuumPsd,
2752 anchorIndex,
2753 config.m_powerLawBlendBins );
2754 if( !!errc )
2755 {
2756 return errc;
2757 }
2758 }
2759
2760 processPsd.resize( rawProcessPsd.size() );
2761 repairMask.assign( rawProcessPsd.size(), 1 );
2762 for( size_t n = 0; n < processPsd.size(); ++n )
2763 {
2764 if( config.m_powerLawOnlyAboveFreq > static_cast<realT>( 0 ) && freq[n] >= config.m_powerLawOnlyAboveFreq )
2765 {
2766 processPsd[n] = std::max( continuumPsd[n], tiny );
2767 repairMask[n] = 0;
2768 }
2769 else if( hardAutoHandoff )
2770 {
2771 processPsd[n] = std::max( rawProcessPsd[n], tiny );
2772 }
2773 else if( rawProcessPsd[n] > noisePsd[n] )
2774 {
2775 processPsd[n] = std::max( rawProcessPsd[n], tiny );
2776 }
2777 else
2778 {
2779 processPsd[n] = std::max( extrapolatedPsd[n], tiny );
2780 }
2781 }
2782
2783 return mx::error_t::noerror;
2784}
2785
2786template <typename realT>
2787mx::error_t modalPsdProcessor<realT>::estimateProcessPsdPowerLawOnly( std::vector<realT> &processPsd,
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,
2795 const processModelConfig &config,
2796 realT *usedPowerLawIndex,
2797 size_t *fitBinsUsed )
2798{
2799 if( measuredPsd.size() != noisePsd.size() || measuredPsd.size() != freq.size() )
2800 {
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" );
2804 }
2805
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 )
2809 {
2810 rawProcessPsd[n] = std::max( measuredPsd[n] - noisePsd[n], tiny );
2811 }
2812
2813 std::vector<realT> continuumPsd;
2814 mx::error_t errc = estimatePowerLawContinuum( continuumPsd,
2815 extrapolation,
2816 anchorIndex,
2817 rawProcessPsd,
2818 anchorProcessPsd,
2819 noisePsd,
2820 freq,
2821 config.m_powerLawIndex,
2822 config.m_powerLawNormFreq,
2823 config.m_powerLawMatchFreq,
2825 config.m_fitPowerLawIndex,
2830 usedPowerLawIndex,
2831 fitBinsUsed );
2832 if( !!errc )
2833 {
2834 return errc;
2835 }
2836
2837 if( normalizePowerLawCrossoverMode( config.m_powerLawCrossoverMode ) == "auto-smoothed-crossing" &&
2838 config.m_powerLawMatchFreq > static_cast<realT>( 0 ) )
2839 {
2840 realT exactPowerLawIndex = config.m_powerLawIndex;
2841 if( usedPowerLawIndex != nullptr )
2842 {
2843 exactPowerLawIndex = *usedPowerLawIndex;
2844 }
2845
2846 errc = matchPowerLawAtFreq( extrapolation,
2847 anchorProcessPsd,
2848 freq,
2849 exactPowerLawIndex,
2850 config.m_powerLawNormFreq,
2851 config.m_powerLawMatchFreq,
2852 static_cast<realT>( 0 ) );
2853 if( !!errc )
2854 {
2855 return errc;
2856 }
2857
2858 errc =
2859 buildPowerLawContinuum( continuumPsd, extrapolation, freq, exactPowerLawIndex, config.m_powerLawNormFreq );
2860 if( !!errc )
2861 {
2862 return errc;
2863 }
2864 }
2865
2866 return buildPowerLawOnlyProcessFromContinuum( processPsd,
2867 repairMask,
2868 rawProcessPsd,
2869 noisePsd,
2870 continuumPsd,
2871 freq,
2872 anchorIndex,
2873 config );
2874}
2875
2876template <typename realT>
2877mx::error_t modalPsdProcessor<realT>::estimateProcessPsdMoffatPeaks( std::vector<realT> &processPsd,
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,
2885 const processModelConfig &config )
2886{
2887 if( measuredPsd.size() != noisePsd.size() || measuredPsd.size() != freq.size() )
2888 {
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" );
2892 }
2893
2894 if( measuredPsd.size() < 3 )
2895 {
2896 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr, "At least three frequency bins are required" );
2897 }
2898
2899 if( config.m_peakMoffatBeta <= static_cast<realT>( 0 ) )
2900 {
2901 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg, "Peak Moffat beta must be positive" );
2902 }
2903
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 )
2907 {
2908 rawProcessPsd[n] = std::max( measuredPsd[n] - noisePsd[n], tiny );
2909 }
2910
2911 std::vector<realT> continuumPsd;
2912 size_t anchorIndex = 0;
2913 mx::error_t errc = estimatePowerLawContinuum( continuumPsd,
2914 extrapolation,
2915 anchorIndex,
2916 rawProcessPsd,
2917 anchorProcessPsd,
2918 noisePsd,
2919 freq,
2920 config.m_powerLawIndex,
2921 config.m_powerLawNormFreq,
2922 config.m_powerLawMatchFreq,
2924 config.m_fitPowerLawIndex,
2929 if( !!errc )
2930 {
2931 return errc;
2932 }
2933
2934 return buildMoffatProcessFromContinuum( processPsd,
2935 peaks,
2936 repairMask,
2937 rawProcessPsd,
2938 noisePsd,
2939 continuumPsd,
2940 freq,
2941 anchorIndex,
2942 config );
2943}
2944
2945template <typename realT>
2946mx::error_t modalPsdProcessor<realT>::estimateProcessPsd( std::vector<realT> &processPsd,
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 )
2954{
2955 if( measuredPsd.size() != noisePsd.size() || measuredPsd.size() != freq.size() )
2956 {
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" );
2960 }
2961
2962 if( measuredPsd.size() < 2 )
2963 {
2964 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr, "At least two frequency bins are required" );
2965 }
2966
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 )
2972 {
2973 processPsd[f] = measuredPsd[f] - noisePsd[f];
2974 }
2975
2976 extrapolation = 0;
2977 int nExtrap = 0;
2978
2979 size_t fMax = static_cast<size_t>( static_cast<realT>( 0.05 ) * static_cast<realT>( freq.size() ) );
2980 if( fMax < 2 )
2981 {
2982 fMax = std::min<size_t>( freq.size(), 2 );
2983 }
2984
2985 for( size_t f = 1; f < fMax; ++f )
2986 {
2987 if( processPsd[f] <= static_cast<realT>( 0.1 ) * noisePsd[f] )
2988 {
2989 continue;
2990 }
2991
2992 extrapolation += log10( processPsd[f] * pow( freq[f] / refFreq, c_defaultPowerLawIndex ) );
2993 ++nExtrap;
2994 }
2995
2996 if( nExtrap > 0 )
2997 {
2998 extrapolation = pow( static_cast<realT>( 10 ), extrapolation / static_cast<realT>( nExtrap ) );
2999 }
3000 else
3001 {
3002 const realT useFreq = freq[refIndex] > static_cast<realT>( 0 ) ? freq[refIndex] : refFreq;
3003 extrapolation =
3004 std::max( processPsd[refIndex] * static_cast<realT>( pow( useFreq / refFreq, c_defaultPowerLawIndex ) ),
3005 tiny );
3006 }
3007
3008 mx::error_t errc = matchPowerLawAtFreq( extrapolation,
3009 processPsd,
3010 freq,
3011 c_defaultPowerLawIndex,
3012 refFreq,
3013 powerLawMatchFreq,
3014 powerLawMatchFallbackWindowHz );
3015 if( !!errc )
3016 {
3017 return errc;
3018 }
3019
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() );
3024
3025 for( size_t f = 0; f < freq.size(); ++f )
3026 {
3027 if( processPsd[f] < static_cast<realT>( 0 ) )
3028 {
3029 const realT useFreq = freq[f] > static_cast<realT>( 0 ) ? freq[f] : refFreq;
3030 toSmooth[f] = extrapolation * pow( refFreq / useFreq, c_defaultPowerLawIndex );
3031 }
3032 else
3033 {
3034 toSmooth[f] = processPsd[f];
3035 }
3036
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] );
3040 }
3041
3042 mx::math::vectorSmoothMean( smoothed, l10, smoothWidths );
3043
3044 for( size_t f = 0; f < freq.size(); ++f )
3045 {
3046 smoothed[f] = pow( static_cast<realT>( 10 ), smoothed[f] );
3047 if( processPsd[f] < noisePsd[f] )
3048 {
3049 processPsd[f] = smoothed[f];
3050 }
3051
3052 processPsd[f] = std::max( processPsd[f], tiny );
3053 }
3054
3055 return mx::error_t::noerror;
3056}
3057
3058template <typename realT>
3059mx::error_t modalPsdProcessor<realT>::fillProcessPsdDropouts( std::vector<realT> &processPsd,
3060 const std::vector<realT> &freq,
3061 const std::vector<unsigned char> &repairMask,
3062 realT gapFactor,
3063 realT tinyFactor,
3064 size_t maxGapBins,
3065 realT powerLawIndex )
3066{
3067 if( processPsd.size() < 3 )
3068 {
3069 return mx::error_t::noerror;
3070 }
3071
3072 if( processPsd.size() != freq.size() )
3073 {
3074 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr,
3075 "Dropout repair frequency grid must match the disturbance PSD size" );
3076 }
3077
3078 if( !repairMask.empty() && repairMask.size() != processPsd.size() )
3079 {
3080 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr,
3081 "Dropout repair mask must match the disturbance PSD size" );
3082 }
3083
3084 if( gapFactor <= static_cast<realT>( 0 ) || gapFactor >= static_cast<realT>( 1 ) )
3085 {
3086 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
3087 "Dropout gap factor must be between 0 and 1" );
3088 }
3089
3090 if( tinyFactor <= static_cast<realT>( 0 ) || tinyFactor >= static_cast<realT>( 1 ) )
3091 {
3092 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg,
3093 "Dropout tiny factor must be between 0 and 1" );
3094 }
3095
3096 const realT tiny = std::numeric_limits<realT>::min();
3097 (void)maxGapBins;
3098
3099 realT refFreq = static_cast<realT>( 1 );
3100 for( size_t n = 0; n < freq.size(); ++n )
3101 {
3102 if( freq[n] > static_cast<realT>( 0 ) )
3103 {
3104 refFreq = freq[n];
3105 break;
3106 }
3107 }
3108
3109 std::vector<realT> sourcePsd = processPsd;
3110 std::vector<realT> updatedPsd = processPsd;
3111 bool changed = false;
3112
3113 if( repairMask.empty() || repairMask[0] != 0 )
3114 {
3115 realT runMax = sourcePsd[0];
3116 for( size_t end = 0; end + 2 < sourcePsd.size(); ++end )
3117 {
3118 if( !repairMask.empty() && repairMask[end] == 0 )
3119 {
3120 break;
3121 }
3122
3123 runMax = std::max( runMax, sourcePsd[end] );
3124
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 )
3130 {
3131 continue;
3132 }
3133
3134 if( runMax > tinyFactor * flankMax )
3135 {
3136 continue;
3137 }
3138
3139 realT xLeft = log10( std::max( freq[end + 1], refFreq ) );
3140 realT xRight = log10( std::max( freq[end + 2], refFreq ) );
3141 if( xRight <= xLeft )
3142 {
3143 xRight = xLeft + static_cast<realT>( 1 );
3144 }
3145
3146 realT yLeft = log10( std::max( sourcePsd[end + 1], tiny ) );
3147 realT yRight = log10( std::max( sourcePsd[end + 2], tiny ) );
3148
3149 for( size_t fill = 0; fill <= end; ++fill )
3150 {
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 );
3156 }
3157
3158 if( updatedPsd.size() > 1 )
3159 {
3160 changed = changed || updatedPsd[0] != updatedPsd[1];
3161 updatedPsd[0] = updatedPsd[1];
3162 }
3163
3164 sourcePsd = updatedPsd;
3165 break;
3166 }
3167 }
3168
3169 size_t n = 1;
3170 while( n + 1 < sourcePsd.size() )
3171 {
3172 if( !repairMask.empty() && repairMask[n] == 0 )
3173 {
3174 ++n;
3175 continue;
3176 }
3177
3178 realT leftVal = std::max( sourcePsd[n - 1], tiny );
3179 if( sourcePsd[n] >= gapFactor * leftVal )
3180 {
3181 ++n;
3182 continue;
3183 }
3184
3185 realT runMax = sourcePsd[n];
3186 bool repaired = false;
3187 for( size_t end = n; end + 1 < sourcePsd.size(); ++end )
3188 {
3189 if( !repairMask.empty() && repairMask[end] == 0 )
3190 {
3191 break;
3192 }
3193
3194 runMax = std::max( runMax, sourcePsd[end] );
3195
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() )
3200 {
3201 canExtend = repairMask[end + 1] != 0;
3202 }
3203
3204 if( sourcePsd[end] >= gapFactor * rightVal )
3205 {
3206 if( canExtend )
3207 {
3208 continue;
3209 }
3210
3211 break;
3212 }
3213
3214 realT flankMin = std::min( leftVal, rightVal );
3215 if( runMax >= gapFactor * flankMin )
3216 {
3217 if( canExtend )
3218 {
3219 continue;
3220 }
3221
3222 break;
3223 }
3224
3225 if( runMax > tinyFactor * flankMax )
3226 {
3227 if( canExtend )
3228 {
3229 continue;
3230 }
3231
3232 break;
3233 }
3234
3235 realT xLeft = log10( std::max( freq[n - 1], refFreq ) );
3236 realT xRight = log10( std::max( freq[end + 1], refFreq ) );
3237 if( xRight <= xLeft )
3238 {
3239 xRight = xLeft + static_cast<realT>( 1 );
3240 }
3241
3242 realT yLeft = log10( std::max( sourcePsd[n - 1], tiny ) );
3243 realT yRight = log10( std::max( sourcePsd[end + 1], tiny ) );
3244
3245 for( size_t fill = n; fill <= end; ++fill )
3246 {
3247 realT xFill = log10( std::max( freq[fill], refFreq ) );
3248 realT alpha = ( xFill - xLeft ) / ( xRight - xLeft );
3249 if( alpha < static_cast<realT>( 0 ) )
3250 {
3251 alpha = static_cast<realT>( 0 );
3252 }
3253 else if( alpha > static_cast<realT>( 1 ) )
3254 {
3255 alpha = static_cast<realT>( 1 );
3256 }
3257
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 );
3261 }
3262
3263 repaired = true;
3264 n = end + 1;
3265 break;
3266 }
3267
3268 if( !repaired )
3269 {
3270 ++n;
3271 }
3272 }
3273
3274 if( repairMask.empty() || repairMask.back() != 0 )
3275 {
3276 realT runMax = sourcePsd.back();
3277 for( size_t offset = 0; offset + 2 < sourcePsd.size(); ++offset )
3278 {
3279 size_t start = sourcePsd.size() - 1 - offset;
3280 if( !repairMask.empty() && repairMask[start] == 0 )
3281 {
3282 break;
3283 }
3284
3285 runMax = std::max( runMax, sourcePsd[start] );
3286
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 )
3292 {
3293 continue;
3294 }
3295
3296 if( runMax > tinyFactor * flankMax )
3297 {
3298 continue;
3299 }
3300
3301 realT yRight = log10( std::max( sourcePsd[start - 1], tiny ) );
3302
3303 for( size_t fill = start; fill < sourcePsd.size(); ++fill )
3304 {
3305 realT useFreq = std::max( freq[fill], refFreq );
3306 realT lastGoodFreq = std::max( freq[start - 1], refFreq );
3307 realT fillValue =
3308 pow( static_cast<realT>( 10 ), yRight ) * pow( lastGoodFreq / useFreq, powerLawIndex );
3309 changed = changed || fillValue != updatedPsd[fill];
3310 updatedPsd[fill] = std::max( fillValue, tiny );
3311 }
3312
3313 sourcePsd = updatedPsd;
3314 break;
3315 }
3316 }
3317
3318 if( changed )
3319 {
3320 processPsd.swap( updatedPsd );
3321 }
3322
3323 return mx::error_t::noerror;
3324}
3325
3326template <typename realT>
3327mx::error_t modalPsdProcessor<realT>::applyLpContinuum( std::vector<realT> &lpProcessPsd,
3328 const std::vector<realT> &processPsd,
3329 const std::vector<realT> &freq,
3330 realT cutoffFreq,
3331 realT continuumWidthHz )
3332{
3333 if( processPsd.size() != freq.size() )
3334 {
3335 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr,
3336 "Process PSD and frequency grid must be the same size" );
3337 }
3338
3339 lpProcessPsd = processPsd;
3340 if( cutoffFreq <= static_cast<realT>( 0 ) )
3341 {
3342 return mx::error_t::noerror;
3343 }
3344
3345 if( continuumWidthHz <= static_cast<realT>( 0 ) )
3346 {
3347 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg, "LP continuum width must be positive" );
3348 }
3349
3350 if( freq.size() < 2 )
3351 {
3352 return mx::error_report<mx::verbose::d>( mx::error_t::sizeerr, "At least two frequency bins are required" );
3353 }
3354
3355 auto cutIt = std::lower_bound( freq.begin(), freq.end(), cutoffFreq );
3356 if( cutIt == freq.end() )
3357 {
3358 return mx::error_t::noerror;
3359 }
3360
3361 size_t cutIndex = cutIt - freq.begin();
3362 if( cutIndex >= lpProcessPsd.size() )
3363 {
3364 return mx::error_t::noerror;
3365 }
3366
3367 const realT df = freq[1] - freq[0];
3368 if( df <= static_cast<realT>( 0 ) )
3369 {
3370 return mx::error_report<mx::verbose::d>( mx::error_t::invalidarg, "Frequency spacing must be positive" );
3371 }
3372
3373 int win = std::max( 3, static_cast<int>( std::lround( continuumWidthHz / df ) ) );
3374 if( win % 2 == 0 )
3375 {
3376 ++win;
3377 }
3378
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 )
3382 {
3383 logTail[n - cutIndex] = log10( std::max( processPsd[n], tiny ) );
3384 }
3385
3386 std::vector<realT> logMedianTail;
3387 std::vector<realT> logContinuumTail;
3388 mx::math::vectorSmoothMedian( logMedianTail, logTail, win );
3389 mx::math::vectorSmoothMean( logContinuumTail, logMedianTail, win );
3390
3391 for( size_t n = cutIndex; n < lpProcessPsd.size(); ++n )
3392 {
3393 lpProcessPsd[n] =
3394 std::max( static_cast<realT>( pow( static_cast<realT>( 10 ), logContinuumTail[n - cutIndex] ) ), tiny );
3395 }
3396
3397 return mx::error_t::noerror;
3398}
3399
3400} // namespace app
3401} // namespace MagAOX
3402
3403#endif // modalPsdProcessor_hpp
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.
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.
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.
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.
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.
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_powerLawNormFreq
The normalization frequency for the power-law continuum.
std::string m_closedLoopOlEstimateMethod
Which CL-to-OL reconstruction method was used.
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.
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.
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_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.
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.
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.
static std::string normalizeClosedLoopOlEstimateMethod(std::string method)
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.
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.
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.
go_lp b(m_lp.m_c)
go_lp a(m_lp.m_c)
Definition dm.hpp:19