API
 
Loading...
Searching...
No Matches
modalGainOpt_test.cpp
Go to the documentation of this file.
1/** \file modalGainOpt_test.cpp
2 * \brief Catch2 tests for the modalGainOpt app.
3 * \author Jared R. Males (jaredmales@gmail.com)
4 *
5 * \ingroup modalGainOpt_files
6 */
7
8#include "../../../tests/testXWC.hpp"
9
10#include "../modalGainOpt.hpp"
11
12using namespace MagAOX::app;
13
14namespace libXWCTest
15{
16
17/** \defgroup modalGainOpt_unit_test modalGainOpt Unit Tests
18 * \brief Unit tests for the modalGainOpt application.
19 *
20 * \ingroup application_unit_test
21 */
22
23/// Namespace for `modalGainOpt` unit tests.
24/** \ingroup modalGainOpt_unit_test
25 */
26namespace modalGainOptTest
27{
28
39
40/// \cond
41class modalGainOptHarness : public modalGainOpt
42{
43 public:
44 void readConfigFile( const std::string &path )
45 {
46 config.readConfig( path );
47 }
48
49 int shutdownState() const
50 {
51 return m_shutdown;
52 }
53
54 float gainGain() const
55 {
56 return m_gainGain;
57 }
58
59 float gainLeak() const
60 {
61 return m_gainLeak;
62 }
63
64 int extrapMethod() const
65 {
66 return m_extrapOL;
67 }
68
69 int extrapNoiseEstimateDomain() const
70 {
71 return m_extrapNoiseEstimateDomain;
72 }
73
74 int extrapNoiseEstimateRange() const
75 {
76 return m_extrapNoiseEstimateRange;
77 }
78
79 int extrapNoiseEstimateStatistic() const
80 {
81 return m_extrapNoiseEstimateStatistic;
82 }
83
84 int extrapClosedLoopOlEstimateMethod() const
85 {
86 return m_extrapClosedLoopOlEstimateMethod;
87 }
88
89 int extrapPowerLawCrossoverMode() const
90 {
91 return m_extrapPowerLawCrossoverMode;
92 }
93
94 void setExtrapMethodForTest( int method )
95 {
96 m_extrapOL = method;
97 }
98
99 void setExtrapNoiseEstimateDomainForTest( int domain )
100 {
101 m_extrapNoiseEstimateDomain = domain;
102 m_extrapConfig.m_noiseEstimateDomain = extrapNoiseEstimateDomainName( domain );
103 }
104
105 void setExtrapNoiseEstimateRangeForTest( int range )
106 {
107 m_extrapNoiseEstimateRange = range;
108 m_extrapConfig.m_noiseEstimateRange = extrapNoiseEstimateRangeName( range );
109 }
110
111 void setExtrapNoiseEstimateStatisticForTest( int statistic )
112 {
113 m_extrapNoiseEstimateStatistic = statistic;
114 m_extrapConfig.m_noiseEstimateStatistic = extrapNoiseEstimateStatisticName( statistic );
115 }
116
117 void setExtrapClosedLoopOlEstimateMethodForTest( int method )
118 {
119 m_extrapClosedLoopOlEstimateMethod = method;
120 m_extrapConfig.m_closedLoopOlEstimateMethod = extrapClosedLoopOlEstimateMethodName( method );
121 }
122
123 void setExtrapPowerLawCrossoverModeForTest( int mode )
124 {
125 m_extrapPowerLawCrossoverMode = mode;
126 m_extrapConfig.m_powerLawCrossoverMode = extrapPowerLawCrossoverModeName( mode );
127 }
128
129 const processPsdProcessorT::processModelConfig &extrapConfig() const
130 {
131 return m_extrapConfig;
132 }
133
134 bool autoUpdate() const
135 {
136 return m_autoUpdate;
137 }
138
139 int modesOn() const
140 {
141 return m_modesOn;
142 }
143
144 int sinceChange() const
145 {
146 return m_sinceChange;
147 }
148
149 bool goptUpdated() const
150 {
151 return m_goptUpdated;
152 }
153
154 bool pcgoptUpdated() const
155 {
156 return m_pcgoptUpdated;
157 }
158
159 bool freqUpdated() const
160 {
161 return m_freqUpdated;
162 }
163
164 float fps() const
165 {
166 return m_fps;
167 }
168
169 const std::vector<float> &freq() const
170 {
171 return m_freq;
172 }
173
174 void setPcOnForTest( bool pcOn )
175 {
176 m_pcOn = pcOn;
177 }
178
179 void setLoopForTest( bool loop )
180 {
181 m_loop = loop;
182 }
183
184 void setModesOnForTest( int modesOn )
185 {
186 m_modesOn = modesOn;
187 }
188
189 void setSinceChangeForTest( int sinceChange )
190 {
191 m_sinceChange = sinceChange;
192 }
193
194 void setGoptUpdatedForTest( bool goptUpdated )
195 {
196 m_goptUpdated = goptUpdated;
197 }
198
199 void setPcgoptUpdatedForTest( bool pcgoptUpdated )
200 {
201 m_pcgoptUpdated = pcgoptUpdated;
202 }
203
204 void setFreqUpdatedForTest( bool freqUpdated )
205 {
206 m_freqUpdated = freqUpdated;
207 }
208
209 void setFpsForTest( float fps )
210 {
211 m_fps = fps;
212 }
213
214 void setFreqForTest( const std::vector<float> &freq )
215 {
216 m_freq = freq;
217 }
218
219 void configurePublishedGainState( const std::vector<float> &gainCalFacts,
220 const std::vector<float> &gainSIRaw,
221 const std::vector<float> &gainSI,
222 const std::vector<float> &gainMaxSI,
223 const std::vector<float> &gainLP,
224 const std::vector<float> &gainMaxLP,
225 const std::vector<float> &gainCals,
226 const std::vector<float> &modeVarOL,
227 const std::vector<float> &modeVarSI,
228 const std::vector<float> &modeVarLP,
229 float opticalGain )
230 {
231 m_gainCalFacts = gainCalFacts;
232 m_optGainSIRaw = gainSIRaw;
233 m_optGainSI = gainSI;
234 m_gmaxSI = gainMaxSI;
235 m_optGainLP = gainLP;
236 m_gmaxLP = gainMaxLP;
237 m_gainCals = gainCals;
238 m_modeVarOL = modeVarOL;
239 m_modeVarSI = modeVarSI;
240 m_modeVarLP = modeVarLP;
241 m_opticalGain = opticalGain;
242 }
243
244 void writePublishedGainArraysForTest( float *currentData,
245 float *siRawData,
246 float *siData,
247 float *maxSiData,
248 float *lpData,
249 float *maxLpData,
250 float *modeVarData )
251 {
252 writePublishedGainArrays( currentData, siRawData, siData, maxSiData, lpData, maxLpData, modeVarData );
253 }
254
255 const std::vector<float> &integratedSiGainsForTest() const
256 {
257 return m_optGainSI;
258 }
259
260 void configureSiIntegratorStateForTest( const std::vector<float> &gainSIRaw,
261 const std::vector<float> &gainSI,
262 float gainGain,
263 float gainLeak )
264 {
265 m_optGainSIRaw = gainSIRaw;
266 m_optGainSI = gainSI;
267 m_gainGain = gainGain;
268 m_gainLeak = gainLeak;
269 }
270
271 void updateIntegratedSiGainForTest( size_t modeIndex )
272 {
273 updateIntegratedSiGain( modeIndex );
274 }
275
276 int requestZeroGainsForTest( bool on = true )
277 {
278 if( on )
279 {
280 std::fill( m_optGainSI.begin(), m_optGainSI.end(), 0.0F );
281 m_siGainStateNeedsSync = false;
282 m_zeroGains = true;
283 }
284 else
285 {
286 m_zeroGains = false;
287 }
288
289 return 0;
290 }
291
292 void initZeroGainsPropertyForTest()
293 {
294 createStandardIndiRequestSw( m_indiP_zeroGains, "zero_gains" );
295 }
296
297 void configurePublishedPredictorState( const std::vector<float> &gainCalFacts,
298 const std::vector<float> &gainLP,
299 const std::vector<float> &gainCals,
300 const std::vector<uint32_t> &Na,
301 const std::vector<uint32_t> &Nb,
302 const std::vector<std::vector<float>> &aCoeff,
303 const std::vector<std::vector<float>> &bCoeff,
304 float opticalGain,
305 float gainGain )
306 {
307 m_gainCalFacts = gainCalFacts;
308 m_optGainLP = gainLP;
309 m_gainCals = gainCals;
310 m_Na = Na;
311 m_Nb = Nb;
312 m_opticalGain = opticalGain;
313 m_gainGain = gainGain;
314
315 m_goptLP.resize( aCoeff.size() );
316 for( size_t n = 0; n < aCoeff.size(); ++n )
317 {
318 m_goptLP[n].a( aCoeff[n] );
319 m_goptLP[n].b( bCoeff[n] );
320 }
321 }
322
323 void writePublishedPredictorArraysForTest(
324 float *pcGainData, float *aCoeffData, uint32_t aWidth, float *bCoeffData, uint32_t bWidth, bool blend )
325 {
326 writePublishedPredictorArrays( pcGainData, aCoeffData, aWidth, bCoeffData, bWidth, blend );
327 }
328
329 int countEnabledGainFactorsForTest( const std::vector<float> &gainFacts ) const
330 {
331 return countEnabledGainFactors( gainFacts );
332 }
333
334 void updateAppliedModeCountForTest( const std::vector<float> &gainFacts, bool predictorPath )
335 {
336 updateAppliedModeCount( gainFacts, predictorPath );
337 }
338
339 bool applyGainFactorUpdateForTest( std::vector<float> &gainFacts,
340 const std::vector<float> &incoming,
341 bool predictorPath )
342 {
343 return applyGainFactorUpdate( gainFacts, incoming.data(), incoming.size(), predictorPath );
344 }
345
346 bool applyMultiplierUpdateForTest( std::vector<float> &multFacts,
347 const std::vector<float> &incoming,
348 bool predictorPath )
349 {
350 return applyMultiplierUpdate( multFacts, incoming.data(), incoming.size(), predictorPath );
351 }
352
353 bool applyFrequencyUpdateForTest( const std::vector<float> &incoming )
354 {
355 return applyFrequencyUpdate( incoming.data(), incoming.size() );
356 }
357
358 void configureGoptStructureInputsForTest( const std::vector<float> &gainFacts,
359 const std::vector<float> &taus,
360 const std::vector<float> &multFacts,
361 const std::vector<float> &freq )
362 {
363 m_gainFacts = gainFacts;
364 m_taus = taus;
365 m_multFacts = multFacts;
366 m_freq = freq;
367 m_gmaxSI.resize( gainFacts.size(), 0.0F );
368 }
369
370 size_t goptCurrentSize() const
371 {
372 return m_goptCurrent.size();
373 }
374
375 bool refreshGoptStructuresForTest()
376 {
377 std::lock_guard<std::mutex> lock( m_goptMutex );
378 return refreshGoptStructures();
379 }
380
381 void initExtrapSelectionPropertiesForTest()
382 {
383 createStandardIndiSelectionSw( m_indiP_extrapMethod,
384 "extrap_method",
393 "Extrapolation Method",
394 "Extrapolation" );
395
396 createStandardIndiSelectionSw( m_indiP_extrapNoiseEstimateDomain,
397 "extrap_noiseEstimateDomain",
402 "Noise Estimate Domain",
403 "Extrapolation" );
404
405 createStandardIndiSelectionSw( m_indiP_extrapNoiseEstimateRange,
406 "extrap_noiseEstimateRange",
411 "Noise Estimate Range",
412 "Extrapolation" );
413
414 createStandardIndiSelectionSw( m_indiP_extrapNoiseEstimateStatistic,
415 "extrap_noiseEstimateStatistic",
420 "Noise Estimate Statistic",
421 "Extrapolation" );
422
423 createStandardIndiSelectionSw(
424 m_indiP_extrapClosedLoopOlEstimateMethod,
425 "extrap_closedLoopOlEstimateMethod",
430 "Closed Loop OL Estimate Method",
431 "Extrapolation" );
432
433 createStandardIndiSelectionSw(
434 m_indiP_extrapPowerLawCrossoverMode,
435 "extrap_powerLawCrossoverMode",
440 "Power-Law Crossover Mode",
441 "Extrapolation" );
442 }
443
444 int handleExtrapMethodPropertyForTest( const pcf::IndiProperty &ipRecv )
445 {
447 }
448
449 int handleExtrapNoiseEstimateDomainPropertyForTest( const pcf::IndiProperty &ipRecv )
450 {
452 }
453
454 int handleExtrapNoiseEstimateRangePropertyForTest( const pcf::IndiProperty &ipRecv )
455 {
457 }
458
459 int handleExtrapNoiseEstimateStatisticPropertyForTest( const pcf::IndiProperty &ipRecv )
460 {
462 }
463
464 int handleExtrapClosedLoopOlEstimateMethodPropertyForTest( const pcf::IndiProperty &ipRecv )
465 {
467 }
468
469 int handleExtrapPowerLawCrossoverModePropertyForTest( const pcf::IndiProperty &ipRecv )
470 {
472 }
473
474 pcf::IndiElement::SwitchStateType extrapMethodElementStateForTest( const std::string &element ) const
475 {
476 return m_indiP_extrapMethod[element].getSwitchState();
477 }
478
479 pcf::IndiElement::SwitchStateType extrapNoiseEstimateDomainElementStateForTest( const std::string &element ) const
480 {
481 return m_indiP_extrapNoiseEstimateDomain[element].getSwitchState();
482 }
483
484 pcf::IndiElement::SwitchStateType extrapNoiseEstimateRangeElementStateForTest( const std::string &element ) const
485 {
486 return m_indiP_extrapNoiseEstimateRange[element].getSwitchState();
487 }
488
489 pcf::IndiElement::SwitchStateType
490 extrapNoiseEstimateStatisticElementStateForTest( const std::string &element ) const
491 {
492 return m_indiP_extrapNoiseEstimateStatistic[element].getSwitchState();
493 }
494
495 pcf::IndiElement::SwitchStateType
496 extrapClosedLoopOlEstimateMethodElementStateForTest( const std::string &element ) const
497 {
498 return m_indiP_extrapClosedLoopOlEstimateMethod[element].getSwitchState();
499 }
500
501 pcf::IndiElement::SwitchStateType extrapPowerLawCrossoverModeElementStateForTest( const std::string &element ) const
502 {
503 return m_indiP_extrapPowerLawCrossoverMode[element].getSwitchState();
504 }
505};
506/// \endcond
507
508/// Verify the placeholder modalGainOpt test harness instantiates the app
509/// cleanly.
510/**
511 * \ingroup modalGainOpt_unit_test
512 */
513TEST_CASE( "modalGainOpt placeholder harness instantiates the app", "[modalGainOpt]" )
514{
515 // clang-format off
516 #ifdef MODALGAINOPT_TEST_DOXYGEN_REF
517 modalGainOpt();
518 #endif
519 // clang-format on
520
521 SECTION( "default construction succeeds" )
522 {
523 modalGainOpt app;
524
525 REQUIRE( true );
526 }
527
528 SECTION( "OL process-method helpers map consistently" )
529 {
530 REQUIRE( olProcessMethodName( c_olProcessNone ) == "none" );
531 REQUIRE( olProcessMethodName( c_olProcessLegacy ) == "legacy" );
532 REQUIRE( olProcessMethodName( c_olProcessPowerLawOnly ) == "power-law-only" );
533 REQUIRE( olProcessMethodName( c_olProcessMoffatPeaks ) == "moffat-peaks" );
534
535 REQUIRE( olProcessMethodFromElement( "none" ) == c_olProcessNone );
536 REQUIRE( olProcessMethodFromElement( "legacy" ) == c_olProcessLegacy );
537 REQUIRE( olProcessMethodFromElement( "power_law_only" ) == c_olProcessPowerLawOnly );
538 REQUIRE( olProcessMethodFromElement( "moffat_peaks" ) == c_olProcessMoffatPeaks );
539
540 REQUIRE( olProcessMethodFromName( "none" ) == c_olProcessNone );
541 REQUIRE( olProcessMethodFromName( "legacy" ) == c_olProcessLegacy );
542 REQUIRE( olProcessMethodFromName( "power_law_only" ) == c_olProcessPowerLawOnly );
543 REQUIRE( olProcessMethodFromName( "power-law-only" ) == c_olProcessPowerLawOnly );
544 REQUIRE( olProcessMethodFromName( "moffat_peaks" ) == c_olProcessMoffatPeaks );
545 REQUIRE( olProcessMethodFromName( "moffat-peaks" ) == c_olProcessMoffatPeaks );
546
548 REQUIRE( extrapNoiseEstimateDomainName( c_extrapNoiseEstimateClosedLoopPreXfer ) == "closed-loop-pre-xfer" );
549
551 REQUIRE( extrapNoiseEstimateDomainFromElement( "closed_loop_pre_xfer" ) ==
553
556 REQUIRE( extrapNoiseEstimateDomainFromName( "closed_loop_pre_xfer" ) ==
558 REQUIRE( extrapNoiseEstimateDomainFromName( "closed-loop-pre-xfer" ) ==
560
569
576
585 }
586}
587
588/// Verify `modalGainOpt::loadConfig()` applies the configured gain update
589/// factor.
590/**
591 * \ingroup modalGainOpt_unit_test
592 */
593TEST_CASE( "modalGainOpt configuration loads PSD-processing settings without "
594 "toggling autoUpdate",
595 "[modalGainOpt]" )
596{
597 modalGainOptHarness app;
598
599 app.setupConfig();
600
601 mx::app::writeConfigFile( "/tmp/modalGainOpt_test.conf",
602 { "loop", "loop", "loop", "loop", "loop",
603 "loop", "extrapolation", "extrapolation", "extrapolation", "extrapolation",
604 "extrapolation", "extrapolation", "extrapolation", "extrapolation", "extrapolation",
605 "extrapolation", "extrapolation", "extrapolation", "extrapolation", "extrapolation",
606 "extrapolation", "extrapolation", "extrapolation", "extrapolation", "extrapolation",
607 "extrapolation", "extrapolation", "extrapolation", "extrapolation", "extrapolation",
608 "extrapolation", "extrapolation", "extrapolation", "extrapolation", "extrapolation",
609 "extrapolation", "extrapolation" },
610 {
611 "number",
612 "name",
613 "autoUpdate",
614 "gainGain",
615 "gainLeak",
616 "psdDev",
617 "method",
618 "noiseEstimateDomain",
619 "noiseEstimateRange",
620 "noiseEstimateStatistic",
621 "noiseEstimateLowFreqMaxHz",
622 "closedLoopOlEstimateMethod",
623 "powerLawIndex",
624 "powerLawNormFreq",
625 "powerLawMatchFreq",
626 "powerLawMatchFallbackWindowHz",
627 "powerLawCrossoverMode",
628 "powerLawAutoSmoothWidthHz",
629 "powerLawAutoMaxFreqFraction",
630 "fitPowerLawIndex",
631 "powerLawOnlyAboveFreq",
632 "powerLawFitIncludesMatchPoint",
633 "powerLawFitMinFreqHz",
634 "powerLawFitMaxFreqHz",
635 "powerLawFitBinWidthHz",
636 "powerLawBlendBins",
637 "peakDetectWidthHz",
638 "peakDetectFactor",
639 "peakDetectBroadFactor",
640 "peakDetectMinWidthLog",
641 "peakDetectPasses",
642 "peakMoffatBeta",
643 "dropoutGapFactor",
644 "dropoutTinyFactor",
645 "dropoutMaxBins",
646 "clSignificanceThreshold",
647 "clMinSignificantFraction",
648 },
649 { "2",
650 "aol2",
651 "false",
652 "0.35",
653 "0.65",
654 "psdDevice",
655 "moffat_peaks",
656 "closed_loop_pre_xfer",
657 "low_freq",
658 "minimum",
659 "123",
660 "ntf_aware",
661 "1.5",
662 "15",
663 "12.5",
664 "7.5",
665 "auto_smoothed_crossing",
666 "37.5",
667 "0.4",
668 "true",
669 "250",
670 "false",
671 "100",
672 "900",
673 "80",
674 "6",
675 "55",
676 "4",
677 "2.5",
678 "0.03",
679 "3",
680 "8",
681 "0.12",
682 "0.000001",
683 "6",
684 "1.25",
685 "0.07" } );
686 app.readConfigFile( "/tmp/modalGainOpt_test.conf" );
687
688 app.loadConfig();
689 // clang-format off
690 #ifdef MODALGAINOPT_TEST_DOXYGEN_REF
693 #endif
694 // clang-format on
695
696 REQUIRE( app.shutdownState() == 0 );
697 REQUIRE( app.autoUpdate() == false );
698 REQUIRE( app.gainGain() == Approx( 0.35F ) );
699 REQUIRE( app.gainLeak() == Approx( 0.65F ) );
700 REQUIRE( app.extrapMethod() == c_olProcessMoffatPeaks );
701 REQUIRE( app.extrapNoiseEstimateDomain() == c_extrapNoiseEstimateClosedLoopPreXfer );
702 REQUIRE( app.extrapNoiseEstimateRange() == c_extrapNoiseEstimateLowFreq );
703 REQUIRE( app.extrapNoiseEstimateStatistic() == c_extrapNoiseEstimateMinimum );
704 REQUIRE( app.extrapClosedLoopOlEstimateMethod() == c_extrapClosedLoopOlEstimateNtfAware );
705 REQUIRE( app.extrapConfig().m_noiseEstimateDomain == "closed-loop-pre-xfer" );
706 REQUIRE( app.extrapConfig().m_noiseEstimateRange == "low-freq" );
707 REQUIRE( app.extrapConfig().m_noiseEstimateStatistic == "minimum" );
708 REQUIRE( app.extrapConfig().m_noiseEstimateLowFreqMaxHz == Approx( 123.0F ) );
709 REQUIRE( app.extrapConfig().m_closedLoopOlEstimateMethod == "ntf-aware" );
710 REQUIRE( app.extrapConfig().m_powerLawIndex == Approx( 1.5F ) );
711 REQUIRE( app.extrapConfig().m_powerLawNormFreq == Approx( 15.0F ) );
712 REQUIRE( app.extrapConfig().m_powerLawMatchFreq == Approx( 12.5F ) );
713 REQUIRE( app.extrapConfig().m_powerLawMatchFallbackWindowHz == Approx( 7.5F ) );
714 REQUIRE( app.extrapConfig().m_powerLawCrossoverMode == "auto-smoothed-crossing" );
715 REQUIRE( app.extrapConfig().m_powerLawAutoSmoothWidthHz == Approx( 37.5F ) );
716 REQUIRE( app.extrapConfig().m_powerLawAutoMaxFreqFraction == Approx( 0.4F ) );
717 REQUIRE( app.extrapConfig().m_fitPowerLawIndex == true );
718 REQUIRE( app.extrapConfig().m_powerLawOnlyAboveFreq == Approx( 250.0F ) );
719 REQUIRE( app.extrapConfig().m_powerLawFitIncludesMatchPoint == false );
720 REQUIRE( app.extrapConfig().m_powerLawFitMinFreqHz == Approx( 100.0F ) );
721 REQUIRE( app.extrapConfig().m_powerLawFitMaxFreqHz == Approx( 900.0F ) );
722 REQUIRE( app.extrapConfig().m_powerLawFitBinWidthHz == Approx( 80.0F ) );
723 REQUIRE( app.extrapConfig().m_powerLawBlendBins == 6 );
724 REQUIRE( app.extrapConfig().m_peakDetectWidthHz == Approx( 55.0F ) );
725 REQUIRE( app.extrapConfig().m_peakDetectFactor == Approx( 4.0F ) );
726 REQUIRE( app.extrapConfig().m_peakDetectBroadFactor == Approx( 2.5F ) );
727 REQUIRE( app.extrapConfig().m_peakDetectMinWidthLog == Approx( 0.03F ) );
728 REQUIRE( app.extrapConfig().m_peakDetectPasses == 3 );
729 REQUIRE( app.extrapConfig().m_peakMoffatBeta == Approx( 8.0F ) );
730 REQUIRE( app.extrapConfig().m_dropoutGapFactor == Approx( 0.12F ) );
731 REQUIRE( app.extrapConfig().m_dropoutTinyFactor == Approx( 1.0e-6F ) );
732 REQUIRE( app.extrapConfig().m_dropoutMaxBins == 6 );
733 REQUIRE( app.extrapConfig().m_clSignificanceThreshold == Approx( 1.25F ) );
734 REQUIRE( app.extrapConfig().m_clMinSignificantFraction == Approx( 0.07F ) );
735}
736
737TEST_CASE( "modalGainOpt restores current selection when extrapolation switches "
738 "receive all-off updates",
739 "[modalGainOpt]" )
740{
741 modalGainOptHarness app;
742 app.initExtrapSelectionPropertiesForTest();
743
744 SECTION( "extrapolation method is restored" )
745 {
746 app.setExtrapMethodForTest( c_olProcessMoffatPeaks );
747
748 pcf::IndiProperty ip( pcf::IndiProperty::Switch );
749 ip.add( pcf::IndiElement( olProcessMethodElement( c_olProcessNone ), pcf::IndiElement::Off ) );
750 ip.add( pcf::IndiElement( olProcessMethodElement( c_olProcessLegacy ), pcf::IndiElement::Off ) );
751 ip.add( pcf::IndiElement( olProcessMethodElement( c_olProcessPowerLawOnly ), pcf::IndiElement::Off ) );
752 ip.add( pcf::IndiElement( olProcessMethodElement( c_olProcessMoffatPeaks ), pcf::IndiElement::Off ) );
753
754 REQUIRE( app.handleExtrapMethodPropertyForTest( ip ) == 0 );
755 REQUIRE( app.extrapMethod() == c_olProcessMoffatPeaks );
756 REQUIRE( app.extrapMethodElementStateForTest( olProcessMethodElement( c_olProcessMoffatPeaks ) ) ==
757 pcf::IndiElement::On );
758 REQUIRE( app.extrapMethodElementStateForTest( olProcessMethodElement( c_olProcessLegacy ) ) ==
759 pcf::IndiElement::Off );
760 }
761
762 SECTION( "noise-estimate domain is restored" )
763 {
764 app.setExtrapNoiseEstimateDomainForTest( c_extrapNoiseEstimateClosedLoopPreXfer );
765
766 pcf::IndiProperty ip( pcf::IndiProperty::Switch );
768 pcf::IndiElement::Off ) );
770 pcf::IndiElement::Off ) );
771
772 REQUIRE( app.handleExtrapNoiseEstimateDomainPropertyForTest( ip ) == 0 );
773 REQUIRE( app.extrapNoiseEstimateDomain() == c_extrapNoiseEstimateClosedLoopPreXfer );
774 REQUIRE( app.extrapConfig().m_noiseEstimateDomain == "closed-loop-pre-xfer" );
775 REQUIRE( app.extrapNoiseEstimateDomainElementStateForTest( extrapNoiseEstimateDomainElement(
776 c_extrapNoiseEstimateClosedLoopPreXfer ) ) == pcf::IndiElement::On );
777 REQUIRE( app.extrapNoiseEstimateDomainElementStateForTest(
779 }
780
781 SECTION( "noise-estimate range is restored" )
782 {
783 app.setExtrapNoiseEstimateRangeForTest( c_extrapNoiseEstimateLowFreq );
784
785 pcf::IndiProperty ip( pcf::IndiProperty::Switch );
787 pcf::IndiElement::Off ) );
789 pcf::IndiElement::Off ) );
790
791 REQUIRE( app.handleExtrapNoiseEstimateRangePropertyForTest( ip ) == 0 );
792 REQUIRE( app.extrapNoiseEstimateRange() == c_extrapNoiseEstimateLowFreq );
793 REQUIRE( app.extrapConfig().m_noiseEstimateRange == "low-freq" );
794 REQUIRE( app.extrapNoiseEstimateRangeElementStateForTest(
796 REQUIRE( app.extrapNoiseEstimateRangeElementStateForTest(
798 }
799
800 SECTION( "noise-estimate statistic is restored" )
801 {
802 app.setExtrapNoiseEstimateStatisticForTest( c_extrapNoiseEstimateMinimum );
803
804 pcf::IndiProperty ip( pcf::IndiProperty::Switch );
806 pcf::IndiElement::Off ) );
808 pcf::IndiElement::Off ) );
809
810 REQUIRE( app.handleExtrapNoiseEstimateStatisticPropertyForTest( ip ) == 0 );
811 REQUIRE( app.extrapNoiseEstimateStatistic() == c_extrapNoiseEstimateMinimum );
812 REQUIRE( app.extrapConfig().m_noiseEstimateStatistic == "minimum" );
813 REQUIRE( app.extrapNoiseEstimateStatisticElementStateForTest(
815 REQUIRE( app.extrapNoiseEstimateStatisticElementStateForTest( extrapNoiseEstimateStatisticElement(
816 c_extrapNoiseEstimatePercentile ) ) == pcf::IndiElement::Off );
817 }
818
819 SECTION( "closed-loop OL estimate method is restored" )
820 {
821 app.setExtrapClosedLoopOlEstimateMethodForTest( c_extrapClosedLoopOlEstimateNtfAware );
822
823 pcf::IndiProperty ip( pcf::IndiProperty::Switch );
825 pcf::IndiElement::Off ) );
827 pcf::IndiElement::Off ) );
828
829 REQUIRE( app.handleExtrapClosedLoopOlEstimateMethodPropertyForTest( ip ) == 0 );
830 REQUIRE( app.extrapClosedLoopOlEstimateMethod() == c_extrapClosedLoopOlEstimateNtfAware );
831 REQUIRE( app.extrapConfig().m_closedLoopOlEstimateMethod == "ntf-aware" );
832 REQUIRE( app.extrapClosedLoopOlEstimateMethodElementStateForTest( extrapClosedLoopOlEstimateMethodElement(
833 c_extrapClosedLoopOlEstimateNtfAware ) ) == pcf::IndiElement::On );
834 REQUIRE( app.extrapClosedLoopOlEstimateMethodElementStateForTest( extrapClosedLoopOlEstimateMethodElement(
835 c_extrapClosedLoopOlEstimateEtfOnly ) ) == pcf::IndiElement::Off );
836 }
837
838 SECTION( "power-law crossover mode is restored" )
839 {
840 app.setExtrapPowerLawCrossoverModeForTest( c_extrapPowerLawCrossoverAutoSmoothedCrossing );
841
842 pcf::IndiProperty ip( pcf::IndiProperty::Switch );
844 pcf::IndiElement::Off ) );
846 pcf::IndiElement::Off ) );
847
848 REQUIRE( app.handleExtrapPowerLawCrossoverModePropertyForTest( ip ) == 0 );
849 REQUIRE( app.extrapPowerLawCrossoverMode() == c_extrapPowerLawCrossoverAutoSmoothedCrossing );
850 REQUIRE( app.extrapConfig().m_powerLawCrossoverMode == "auto-smoothed-crossing" );
851 REQUIRE( app.extrapPowerLawCrossoverModeElementStateForTest( extrapPowerLawCrossoverModeElement(
852 c_extrapPowerLawCrossoverAutoSmoothedCrossing ) ) == pcf::IndiElement::On );
853 REQUIRE( app.extrapPowerLawCrossoverModeElementStateForTest(
855 }
856}
857
858TEST_CASE( "modalPsdProcessor falls back when the requested power-law fit has "
859 "too little frequency span",
860 "[modalGainOpt]" )
861{
863 cfg.m_method = "moffat-peaks";
864 cfg.m_powerLawIndex = 1.5F;
865 cfg.m_powerLawMatchFreq = 25.0F;
866 cfg.m_fitPowerLawIndex = true;
867 cfg.m_powerLawFitMinFreqHz = 25.0F;
868 cfg.m_powerLawFitMaxFreqHz = 1000.0F;
869 cfg.m_powerLawFitBinWidthHz = 100.0F;
870 cfg.m_powerLawBlendBins = 5;
871
872 std::vector<float> measuredPsd{ 1.0e-6F, 9.0e-7F, 8.0e-7F, 7.0e-7F, 6.0e-7F };
873 std::vector<float> freq{ 0.0F, 25.0F, 50.0F, 75.0F, 100.0F };
874
876 mx::error_t errc = processPsdProcessorT::analyzePsd( result, measuredPsd, freq, 10, cfg, 0.0F, 25.0F );
877
878 REQUIRE( !errc );
879 REQUIRE( result.m_powerLawIndex == Approx( cfg.m_powerLawIndex ) );
880 REQUIRE( result.m_powerLawIndexFitSucceeded == false );
881 REQUIRE( result.m_powerLawFitBinsUsed == 0 );
882}
883
884TEST_CASE( "modalPsdProcessor can estimate noise in closed-loop space before OL "
885 "correction",
886 "[modalGainOpt]" )
887{
889 cfg.m_method = "legacy";
890 cfg.m_noiseEstimateDomain = "closed_loop_pre_xfer";
891
892 std::vector<float> measuredPsd{ 0.0F, 5.0F, 5.0F, 1.0F, 1.0F, 1.0F, 1.0F, 1.0F };
893 std::vector<float> freq{ 0.0F, 1.0F, 2.0F, 3.0F, 4.0F, 5.0F, 6.0F, 7.0F };
894 std::vector<float> correctionPsd( measuredPsd.size(), 0.5F );
895 correctionPsd[0] = 1.0F;
896
898 mx::error_t errc =
899 processPsdProcessorT::analyzePsd( result, measuredPsd, freq, 10, cfg, 0.0F, 25.0F, &correctionPsd );
900
901 REQUIRE( !errc );
902 REQUIRE( result.m_noiseEstimateDomain == "closed-loop-pre-xfer" );
903 REQUIRE( result.m_noiseFloor == Approx( 1.0F ) );
904 REQUIRE( result.m_noisePsd[1] == Approx( 1.0F ) );
905 REQUIRE( result.m_processPsd[1] == Approx( 8.0F ) );
906}
907
908TEST_CASE( "modalPsdProcessor can estimate noise from the low-frequency end", "[modalGainOpt]" )
909{
910 std::vector<float> measuredPsd{ 0.0F, 2.0F, 2.0F, 20.0F, 20.0F, 20.0F, 20.0F, 20.0F };
911 std::vector<float> freq{ 0.0F, 1.0F, 2.0F, 3.0F, 4.0F, 5.0F, 6.0F, 7.0F };
912 std::vector<float> noisePsd;
913 float noiseFloor = 0.0F;
914
915 mx::error_t errc =
916 processPsdProcessorT::estimateNoisePsd( noisePsd, noiseFloor, measuredPsd, freq, 10, "low_freq" );
917
918 REQUIRE( !errc );
919 REQUIRE( noiseFloor == Approx( 2.0F ) );
920 REQUIRE( noisePsd[1] == Approx( 2.0F ) );
921}
922
923TEST_CASE( "modalPsdProcessor can estimate noise using the minimum PSD in range", "[modalGainOpt]" )
924{
925 std::vector<float> measuredPsd{ 0.0F, 8.0F, 4.0F, 2.0F, 20.0F, 20.0F, 20.0F, 20.0F };
926 std::vector<float> freq{ 0.0F, 1.0F, 2.0F, 3.0F, 4.0F, 5.0F, 6.0F, 7.0F };
927 std::vector<float> noisePsd;
928 float noiseFloor = 0.0F;
929
930 mx::error_t errc =
931 processPsdProcessorT::estimateNoisePsd( noisePsd, noiseFloor, measuredPsd, freq, 10, "low_freq", "minimum" );
932
933 REQUIRE( !errc );
934 REQUIRE( noiseFloor == Approx( 2.0F ) );
935 REQUIRE( noisePsd[1] == Approx( 2.0F ) );
936}
937
938TEST_CASE( "modalPsdProcessor can limit low-frequency noise estimation to a max "
939 "frequency",
940 "[modalGainOpt]" )
941{
942 std::vector<float> measuredPsd{ 0.0F, 20.0F, 20.0F, 2.0F, 2.0F, 20.0F, 20.0F, 20.0F, 20.0F, 20.0F };
943 std::vector<float> freq{ 0.0F, 1.0F, 2.0F, 3.0F, 4.0F, 5.0F, 6.0F, 7.0F, 8.0F, 9.0F };
944 std::vector<float> noisePsd;
945 float noiseFloor = 0.0F;
946
947 mx::error_t errc = processPsdProcessorT::estimateNoisePsd( noisePsd,
948 noiseFloor,
949 measuredPsd,
950 freq,
951 10,
952 "low_freq",
953 "percentile",
954 2.1F );
955
956 REQUIRE( !errc );
957 REQUIRE( noiseFloor == Approx( 20.0F ) );
958 REQUIRE( noisePsd[1] == Approx( 20.0F ) );
959}
960
961TEST_CASE( "modalPsdProcessor can reconstruct OL PSD with NTF-aware closed-loop "
962 "noise subtraction",
963 "[modalGainOpt]" )
964{
966 cfg.m_method = "legacy";
967 cfg.m_noiseEstimateDomain = "closed_loop_pre_xfer";
968 cfg.m_closedLoopOlEstimateMethod = "ntf_aware";
969
970 std::vector<float> measuredPsd{ 0.0F, 10.0F, 10.0F, 2.0F, 2.0F, 2.0F, 2.0F, 2.0F };
971 std::vector<float> freq{ 0.0F, 1.0F, 2.0F, 3.0F, 4.0F, 5.0F, 6.0F, 7.0F };
972 std::vector<float> etfPsd( measuredPsd.size(), 0.5F );
973 std::vector<float> ntfPsd( measuredPsd.size(), 2.0F );
974 etfPsd[0] = 1.0F;
975 ntfPsd[0] = 1.0F;
976
978 mx::error_t errc =
979 processPsdProcessorT::analyzePsd( result, measuredPsd, freq, 10, cfg, 0.0F, 25.0F, &etfPsd, &ntfPsd );
980
981 REQUIRE( !errc );
982 REQUIRE( result.m_noiseEstimateDomain == "closed-loop-pre-xfer" );
983 REQUIRE( result.m_closedLoopOlEstimateMethod == "ntf-aware" );
984 REQUIRE( result.m_noiseFloor == Approx( 2.0F ) );
985 REQUIRE( result.m_noisePsd[1] == Approx( 2.0F ) );
986 REQUIRE( result.m_processPsd[1] == Approx( 12.0F ) );
987}
988
989TEST_CASE( "modalPsdProcessor fits closed-loop noise on raw CL PSD even when OL "
990 "reconstruction is NTF-aware",
991 "[modalGainOpt]" )
992{
994 cfg.m_method = "legacy";
995 cfg.m_noiseEstimateDomain = "closed_loop_pre_xfer";
996 cfg.m_noiseEstimateRange = "low_freq";
997 cfg.m_noiseEstimateStatistic = "minimum";
999 cfg.m_closedLoopOlEstimateMethod = "ntf_aware";
1000
1001 std::vector<float> measuredPsd{ 0.0F, 20.0F, 18.0F, 2.0F, 2.0F, 2.0F, 2.0F, 2.0F };
1002 std::vector<float> freq{ 0.0F, 1.0F, 2.0F, 3.0F, 4.0F, 5.0F, 6.0F, 7.0F };
1003 std::vector<float> etfPsd( measuredPsd.size(), 0.5F );
1004 std::vector<float> ntfPsd( measuredPsd.size(), 4.0F );
1005 etfPsd[0] = 1.0F;
1006 ntfPsd[0] = 1.0F;
1007
1009 mx::error_t errc =
1010 processPsdProcessorT::analyzePsd( result, measuredPsd, freq, 10, cfg, 0.0F, 25.0F, &etfPsd, &ntfPsd );
1011
1012 REQUIRE( !errc );
1013 REQUIRE( result.m_noiseFloor == Approx( 2.0F ) );
1014 REQUIRE( result.m_noisePsd[1] == Approx( 2.0F ) );
1015 REQUIRE( result.m_processPsd[1] == Approx( 24.0F ) );
1016}
1017
1018TEST_CASE( "modalPsdProcessor power-law-only matches moffat handoff when forced "
1019 "to pure power law above a cutoff",
1020 "[modalGainOpt]" )
1021{
1022 std::vector<float> measuredPsd{ 0.0F, 11.0F, 9.0F, 7.0F, 5.0F, 4.0F, 3.4F, 3.0F, 2.8F, 2.6F, 2.4F };
1023 std::vector<float> freq{ 0.0F, 20.0F, 40.0F, 60.0F, 80.0F, 100.0F, 120.0F, 140.0F, 160.0F, 180.0F, 200.0F };
1024
1026 powerCfg.m_method = "power-law-only";
1027 powerCfg.m_powerLawIndex = 1.0F;
1028 powerCfg.m_powerLawMatchFreq = 20.0F;
1029 powerCfg.m_powerLawOnlyAboveFreq = 100.0F;
1030 powerCfg.m_noiseEstimateStatistic = "minimum";
1031
1032 processPsdProcessorT::processModelConfig moffatCfg = powerCfg;
1033 moffatCfg.m_method = "moffat-peaks";
1034 moffatCfg.m_peakDetectFactor = 1.0e6F;
1035 moffatCfg.m_peakDetectBroadFactor = 1.0e6F;
1036
1039
1040 mx::error_t errc = processPsdProcessorT::analyzePsd( powerResult, measuredPsd, freq, 10, powerCfg );
1041 REQUIRE( !errc );
1042
1043 errc = processPsdProcessorT::analyzePsd( moffatResult, measuredPsd, freq, 10, moffatCfg );
1044 REQUIRE( !errc );
1045 REQUIRE( moffatResult.m_peaks.empty() );
1046 REQUIRE( powerResult.m_processPsd.size() == moffatResult.m_processPsd.size() );
1047 for( size_t n = 0; n < powerResult.m_processPsd.size(); ++n )
1048 {
1049 REQUIRE( powerResult.m_processPsd[n] == Approx( moffatResult.m_processPsd[n] ) );
1050 }
1051}
1052
1053TEST_CASE( "modalPsdProcessor power-law-only repairs deep dropouts below the "
1054 "pure-power-law cutoff",
1055 "[modalGainOpt]" )
1056{
1057 std::vector<float> measuredPsd{ 0.0F, 11.0F, 10.0F, 2.5F, 9.0F, 8.0F, 7.5F, 7.0F };
1058 std::vector<float> freq{ 0.0F, 20.0F, 40.0F, 60.0F, 80.0F, 100.0F, 120.0F, 140.0F };
1059
1061 cfg.m_method = "power-law-only";
1062 cfg.m_noiseEstimateRange = "low_freq";
1063 cfg.m_noiseEstimateStatistic = "minimum";
1064 cfg.m_powerLawIndex = 1.0F;
1065 cfg.m_powerLawMatchFreq = 20.0F;
1066 cfg.m_powerLawOnlyAboveFreq = 100.0F;
1067
1069 mx::error_t errc = processPsdProcessorT::analyzePsd( result, measuredPsd, freq, 10, cfg );
1070
1071 REQUIRE( !errc );
1072 REQUIRE( result.m_noiseFloor == Approx( 2.5F ) );
1073 REQUIRE( result.m_processPsd[3] > 1.0F );
1074 REQUIRE( result.m_processPsd[5] == Approx( result.m_extrapolation * pow( 20.0F / freq[5], 1.0F ) ) );
1075 REQUIRE( result.m_processPsd[6] == Approx( result.m_extrapolation * pow( 20.0F / freq[6], 1.0F ) ) );
1076 REQUIRE( result.m_processPsd[7] == Approx( result.m_extrapolation * pow( 20.0F / freq[7], 1.0F ) ) );
1077}
1078
1079TEST_CASE( "modalPsdProcessor can auto-select the power-law crossover from a "
1080 "smoothed noise crossing",
1081 "[modalGainOpt]" )
1082{
1083 std::vector<float> measuredPsd{ 0.0F, 10.0F, 8.5F, 6.5F, 4.5F, 3.1F, 2.7F, 2.55F, 4.6F, 2.51F, 2.5F };
1084 std::vector<float> freq{ 0.0F, 20.0F, 40.0F, 60.0F, 80.0F, 100.0F, 120.0F, 140.0F, 160.0F, 180.0F, 200.0F };
1085
1087 cfg.m_method = "power-law-only";
1088 cfg.m_noiseEstimateStatistic = "minimum";
1089 cfg.m_powerLawIndex = 1.0F;
1090 cfg.m_powerLawNormFreq = 20.0F;
1091 cfg.m_powerLawMatchFreq = 0.0F;
1092 cfg.m_powerLawOnlyAboveFreq = 0.0F;
1093 cfg.m_powerLawCrossoverMode = "auto-smoothed-crossing";
1094 cfg.m_powerLawAutoSmoothWidthHz = 100.0F;
1096
1098 mx::error_t errc = processPsdProcessorT::analyzePsd( result, measuredPsd, freq, 10, cfg );
1099
1100 REQUIRE( !errc );
1101 REQUIRE( result.m_powerLawCrossoverMode == "auto-smoothed-crossing" );
1102 REQUIRE( result.m_powerLawMatchFreq == Approx( result.m_powerLawOnlyAboveFreq ) );
1103 REQUIRE( result.m_powerLawMatchFreq > 80.0F );
1104 REQUIRE( result.m_powerLawMatchFreq < 150.0F );
1105 REQUIRE( result.m_processPsd[8] < static_cast<float>( measuredPsd[8] - result.m_noiseFloor ) );
1106}
1107
1108TEST_CASE( "modalPsdProcessor falls back to the highest-frequency smoothed "
1109 "minimum when no noise crossing exists",
1110 "[modalGainOpt]" )
1111{
1112 std::vector<float> smoothedProcessPsd{ 5.0F, 4.0F, 2.0F, 1.5F, 1.5F };
1113 std::vector<float> noisePsd{ 1.0F, 1.0F, 1.0F, 1.0F, 1.0F };
1114 std::vector<float> freq{ 0.0F, 25.0F, 50.0F, 75.0F, 100.0F };
1115
1116 float crossoverFreq = 0.0F;
1117 mx::error_t errc = processPsdProcessorHarness::findAutoPowerLawCrossoverFreq( crossoverFreq,
1118 smoothedProcessPsd,
1119 noisePsd,
1120 freq,
1121 0.0F );
1122
1123 REQUIRE( !errc );
1124 REQUIRE( crossoverFreq == Approx( 100.0F ) );
1125}
1126
1127TEST_CASE( "modalPsdProcessor treats a below-to-above sign change as a valid "
1128 "smoothed noise crossing",
1129 "[modalGainOpt]" )
1130{
1131 std::vector<float> smoothedProcessPsd{ 0.5F, 0.8F, 1.0F, 1.2F, 1.4F };
1132 std::vector<float> noisePsd{ 1.0F, 1.0F, 1.0F, 1.0F, 1.0F };
1133 std::vector<float> freq{ 0.0F, 25.0F, 50.0F, 75.0F, 100.0F };
1134
1135 float crossoverFreq = 0.0F;
1136 mx::error_t errc = processPsdProcessorHarness::findAutoPowerLawCrossoverFreq( crossoverFreq,
1137 smoothedProcessPsd,
1138 noisePsd,
1139 freq,
1140 0.0F );
1141
1142 REQUIRE( !errc );
1143 REQUIRE( crossoverFreq == Approx( 50.0F ) );
1144}
1145
1146TEST_CASE( "modalPsdProcessor auto crossover can cap the search to a fraction "
1147 "of the sampled maximum frequency",
1148 "[modalGainOpt]" )
1149{
1150 std::vector<float> smoothedProcessPsd{ 5.0F, 4.0F, 1.5F, 0.8F, 0.7F, 1.4F, 1.6F };
1151 std::vector<float> noisePsd{ 1.0F, 1.0F, 1.0F, 1.0F, 1.0F, 1.0F, 1.0F };
1152 std::vector<float> freq{ 0.0F, 100.0F, 200.0F, 300.0F, 400.0F, 900.0F, 1000.0F };
1153
1154 float crossoverFreq = 0.0F;
1155 mx::error_t errc = processPsdProcessorHarness::findAutoPowerLawCrossoverFreq( crossoverFreq,
1156 smoothedProcessPsd,
1157 noisePsd,
1158 freq,
1159 0.4F );
1160
1161 REQUIRE( !errc );
1162 REQUIRE( crossoverFreq == Approx( 271.42856F ) );
1163}
1164
1165TEST_CASE( "modalPsdProcessor snaps the effective auto crossover to the next "
1166 "sampled frequency bin",
1167 "[modalGainOpt]" )
1168{
1169 std::vector<float> rawProcessPsd{ 5.0F, 4.0F, 2.0F, 0.8F, 0.7F };
1170 std::vector<float> smoothedProcessPsd{ 5.0F, 4.0F, 2.0F, 0.8F, 0.7F };
1171 std::vector<float> noisePsd{ 1.0F, 1.0F, 1.0F, 1.0F, 1.0F };
1172 std::vector<float> freq{ 0.0F, 100.0F, 200.0F, 300.0F, 400.0F };
1173
1174 float matchFreq = 0.0F;
1175 float onlyAboveFreq = 0.0F;
1177 onlyAboveFreq,
1178 rawProcessPsd,
1179 smoothedProcessPsd,
1180 noisePsd,
1181 freq,
1182 "auto-smoothed-crossing",
1183 0.0F );
1184
1185 REQUIRE( !errc );
1186 REQUIRE( matchFreq == Approx( 300.0F ) );
1187 REQUIRE( onlyAboveFreq == Approx( 300.0F ) );
1188}
1189
1190TEST_CASE( "modalPsdProcessor anchors the power-law match to the smoothed "
1191 "disturbance PSD",
1192 "[modalGainOpt]" )
1193{
1194 std::vector<float> rawProcessPsd{ 1.0F, 1.0F, 100.0F, 0.5F, 0.25F };
1195 std::vector<float> smoothedProcessPsd{ 1.0F, 1.0F, 10.0F, 0.5F, 0.25F };
1196 std::vector<float> noisePsd{ 0.1F, 0.1F, 0.1F, 0.1F, 0.1F };
1197 std::vector<float> freq{ 0.0F, 10.0F, 20.0F, 30.0F, 40.0F };
1198
1199 std::vector<float> continuumPsd;
1200 float extrapolation = 0.0F;
1201 size_t anchorIndex = 0;
1202 mx::error_t errc = processPsdProcessorHarness::estimatePowerLawContinuum( continuumPsd,
1203 extrapolation,
1204 anchorIndex,
1205 rawProcessPsd,
1206 smoothedProcessPsd,
1207 noisePsd,
1208 freq,
1209 1.0F,
1210 10.0F,
1211 20.0F,
1212 0.0F );
1213
1214 REQUIRE( !errc );
1215 REQUIRE( anchorIndex >= 1 );
1216 REQUIRE( extrapolation == Approx( 20.0F ) );
1217}
1218
1219TEST_CASE( "modalPsdProcessor power-law-only auto handoff matches the "
1220 "smoothed crossover exactly without a blend ramp",
1221 "[modalGainOpt]" )
1222{
1223 std::vector<float> measuredPsd{ 1.0F, 1.2F, 41.0F, 31.0F, 3.0F, 2.0F };
1224 std::vector<float> smoothedProcessPsd{ 1.0F, 45.0F, 35.0F, 25.0F, 2.0F, 1.5F };
1225 std::vector<float> noisePsd{ 1.0F, 1.0F, 1.0F, 1.0F, 1.0F, 1.0F };
1226 std::vector<float> freq{ 0.0F, 10.0F, 20.0F, 30.0F, 40.0F, 50.0F };
1227
1229 cfg.m_method = "power-law-only";
1230 cfg.m_powerLawIndex = 1.0F;
1231 cfg.m_powerLawNormFreq = 10.0F;
1232 cfg.m_powerLawMatchFreq = 40.0F;
1233 cfg.m_powerLawOnlyAboveFreq = 40.0F;
1234 cfg.m_powerLawCrossoverMode = "auto-smoothed-crossing";
1236 cfg.m_powerLawBlendBins = 20;
1237
1238 std::vector<float> processPsd;
1239 float extrapolation = 0.0F;
1240 size_t anchorIndex = 0;
1241 std::vector<unsigned char> repairMask;
1242 float usedPowerLawIndex = 0.0F;
1243 size_t fitBinsUsed = 0;
1244
1246 extrapolation,
1247 anchorIndex,
1248 repairMask,
1249 measuredPsd,
1250 smoothedProcessPsd,
1251 noisePsd,
1252 freq,
1253 cfg,
1254 &usedPowerLawIndex,
1255 &fitBinsUsed );
1256
1257 REQUIRE( !errc );
1258 REQUIRE( usedPowerLawIndex == Approx( 1.0F ) );
1259 REQUIRE( processPsd[1] == Approx( measuredPsd[1] - noisePsd[1] ) );
1260 REQUIRE( processPsd[3] == Approx( measuredPsd[3] - noisePsd[3] ) );
1261 REQUIRE( processPsd[4] == Approx( smoothedProcessPsd[4] ) );
1262 REQUIRE( processPsd[5] == Approx( 1.6F ) );
1263}
1264
1265TEST_CASE( "modalPsdProcessor repairs raw disturbance dropouts before "
1266 "extrapolation",
1267 "[modalGainOpt]" )
1268{
1269 std::vector<float> measuredPsd{ 0.0F, 11.0F, 10.0F, 1.2F, 9.0F, 8.0F, 1.0F };
1270 std::vector<float> freq{ 0.0F, 10.0F, 20.0F, 30.0F, 40.0F, 50.0F, 60.0F };
1271
1273 cfg.m_method = "power-law-only";
1274 cfg.m_noiseEstimateStatistic = "minimum";
1275 cfg.m_powerLawIndex = 1.0F;
1276 cfg.m_powerLawNormFreq = 10.0F;
1277 cfg.m_powerLawMatchFreq = 0.0F;
1278 cfg.m_powerLawOnlyAboveFreq = 0.0F;
1279
1281 mx::error_t errc = processPsdProcessorT::analyzePsd( result, measuredPsd, freq, 10, cfg );
1282
1283 REQUIRE( !errc );
1284 REQUIRE( result.m_noiseFloor == Approx( 1.0F ) );
1285 REQUIRE( result.m_rawProcessPsd[3] > 0.2F );
1286}
1287
1288TEST_CASE( "modalPsdProcessor repairs trailing high-frequency dropout runs", "[modalGainOpt]" )
1289{
1290 std::vector<float> processPsd{ 10.0F, 9.0F, 8.0F, 1.0e-8F, 1.0e-8F, 1.0e-8F };
1291 std::vector<float> freq{ 0.0F, 10.0F, 20.0F, 30.0F, 40.0F, 50.0F };
1292
1293 mx::error_t errc =
1294 processPsdProcessorHarness::fillProcessPsdDropouts( processPsd, freq, {}, 0.2F, 1.0e-6F, 4, 1.0F );
1295
1296 REQUIRE( !errc );
1297 REQUIRE( processPsd[3] > 1.0F );
1298 REQUIRE( processPsd[4] > 1.0F );
1299 REQUIRE( processPsd[5] > 1.0F );
1300 REQUIRE( processPsd[3] == Approx( 16.0F / 3.0F ) );
1301 REQUIRE( processPsd[4] == Approx( 4.0F ) );
1302 REQUIRE( processPsd[5] == Approx( 3.2F ) );
1303}
1304
1305TEST_CASE( "modalPsdProcessor keeps trailing dropout repair bounded when the "
1306 "last good bins rise",
1307 "[modalGainOpt]" )
1308{
1309 std::vector<float> processPsd{ 1.0F, 2.0F, 4.0F, 1.0e-8F, 1.0e-8F, 1.0e-8F };
1310 std::vector<float> freq{ 0.0F, 10.0F, 20.0F, 30.0F, 40.0F, 50.0F };
1311
1312 mx::error_t errc =
1313 processPsdProcessorHarness::fillProcessPsdDropouts( processPsd, freq, {}, 0.2F, 1.0e-6F, 4, 1.0F );
1314
1315 REQUIRE( !errc );
1316 REQUIRE( processPsd[3] == Approx( 8.0F / 3.0F ) );
1317 REQUIRE( processPsd[4] == Approx( 2.0F ) );
1318 REQUIRE( processPsd[5] == Approx( 1.6F ) );
1319}
1320
1321TEST_CASE( "modalPsdProcessor does not treat a sharp post-peak decline as a "
1322 "dropout unless it is truly tiny",
1323 "[modalGainOpt]" )
1324{
1325 std::vector<float> processPsd{ 1.0e-4F, 1.0e-6F, 1.0e-8F, 1.0e-8F, 1.0e-8F };
1326 std::vector<float> freq{ 0.0F, 10.0F, 20.0F, 30.0F, 40.0F };
1327
1328 mx::error_t errc =
1329 processPsdProcessorHarness::fillProcessPsdDropouts( processPsd, freq, {}, 0.2F, 1.0e-6F, 4, 1.0F );
1330
1331 REQUIRE( !errc );
1332 REQUIRE( processPsd[2] == Approx( 1.0e-8F ) );
1333 REQUIRE( processPsd[3] == Approx( 1.0e-8F ) );
1334 REQUIRE( processPsd[4] == Approx( 1.0e-8F ) );
1335}
1336
1337/// Verify `modalGainOpt` publishes LP and max-gain arrays into separate
1338/// buffers.
1339/**
1340 * \ingroup modalGainOpt_unit_test
1341 */
1342TEST_CASE( "modalGainOpt published gain arrays keep LP and max LP outputs distinct", "[modalGainOpt]" )
1343{
1344 modalGainOptHarness app;
1345
1346 app.configurePublishedGainState( { 2.0F, 4.0F },
1347 { 29.0F, 31.0F },
1348 { 3.0F, 5.0F },
1349 { 7.0F, 11.0F },
1350 { 13.0F, 17.0F },
1351 { 19.0F, 23.0F },
1352 { 1.0F, 2.0F },
1353 { 0.1F, 0.2F },
1354 { 1.1F, 1.2F },
1355 { 2.1F, 2.2F },
1356 2.0F );
1357
1358 std::vector<float> currentData( 2, -1.0F );
1359 std::vector<float> siRawData( 2, -1.0F );
1360 std::vector<float> siData( 2, -1.0F );
1361 std::vector<float> maxSiData( 2, -1.0F );
1362 std::vector<float> lpData( 2, -1.0F );
1363 std::vector<float> maxLpData( 2, -1.0F );
1364 std::vector<float> modeVarData( 6, -1.0F );
1365
1366 app.writePublishedGainArraysForTest( currentData.data(),
1367 siRawData.data(),
1368 siData.data(),
1369 maxSiData.data(),
1370 lpData.data(),
1371 maxLpData.data(),
1372 modeVarData.data() );
1373
1374 mx::improc::eigenMap<float> modeVars( modeVarData.data(), 3, 2 );
1375
1376 REQUIRE( currentData[0] == Approx( 3.0F ) );
1377 REQUIRE( currentData[1] == Approx( 5.0F ) );
1378 REQUIRE( siRawData[0] == Approx( 29.0F ) );
1379 REQUIRE( siRawData[1] == Approx( 31.0F ) );
1380 REQUIRE( siData[0] == Approx( 3.0F ) );
1381 REQUIRE( siData[1] == Approx( 5.0F ) );
1382 REQUIRE( maxSiData[0] == Approx( 7.0F ) );
1383 REQUIRE( maxSiData[1] == Approx( 11.0F ) );
1384 REQUIRE( lpData[0] == Approx( 13.0F ) );
1385 REQUIRE( lpData[1] == Approx( 17.0F ) );
1386 REQUIRE( maxLpData[0] == Approx( 19.0F ) );
1387 REQUIRE( maxLpData[1] == Approx( 23.0F ) );
1388 REQUIRE( modeVars( 0, 0 ) == Approx( 0.1F ) );
1389 REQUIRE( modeVars( 1, 0 ) == Approx( 1.1F ) );
1390 REQUIRE( modeVars( 2, 0 ) == Approx( 2.1F ) );
1391 REQUIRE( modeVars( 0, 1 ) == Approx( 0.2F ) );
1392 REQUIRE( modeVars( 1, 1 ) == Approx( 1.2F ) );
1393 REQUIRE( modeVars( 2, 1 ) == Approx( 2.2F ) );
1394}
1395
1396/// Verify `modalGainOpt` applies gain calibration and optical-gain scaling when
1397/// publishing gains.
1398/**
1399 * \ingroup modalGainOpt_unit_test
1400 */
1401TEST_CASE( "modalGainOpt published gain arrays apply calibration scaling", "[modalGainOpt]" )
1402{
1403 modalGainOptHarness app;
1404
1405 app.configurePublishedGainState( { 6.0F, 3.0F },
1406 { 2.0F, 10.0F },
1407 { 4.0F, 12.0F },
1408 { 8.0F, 18.0F },
1409 { 10.0F, 20.0F },
1410 { 14.0F, 24.0F },
1411 { 3.0F, 6.0F },
1412 { 0.4F, 0.8F },
1413 { 1.4F, 1.8F },
1414 { 2.4F, 2.8F },
1415 4.0F );
1416
1417 std::vector<float> currentData( 2, -1.0F );
1418 std::vector<float> siRawData( 2, -1.0F );
1419 std::vector<float> siData( 2, -1.0F );
1420 std::vector<float> maxSiData( 2, -1.0F );
1421 std::vector<float> lpData( 2, -1.0F );
1422 std::vector<float> maxLpData( 2, -1.0F );
1423 std::vector<float> modeVarData( 6, -1.0F );
1424
1425 app.writePublishedGainArraysForTest( currentData.data(),
1426 siRawData.data(),
1427 siData.data(),
1428 maxSiData.data(),
1429 lpData.data(),
1430 maxLpData.data(),
1431 modeVarData.data() );
1432
1433 REQUIRE( currentData[0] == Approx( 2.0F ) );
1434 REQUIRE( currentData[1] == Approx( 1.5F ) );
1435 REQUIRE( siRawData[0] == Approx( 1.0F ) );
1436 REQUIRE( siRawData[1] == Approx( 1.25F ) );
1437 REQUIRE( siData[0] == Approx( 2.0F ) );
1438 REQUIRE( siData[1] == Approx( 1.5F ) );
1439 REQUIRE( maxSiData[0] == Approx( 4.0F ) );
1440 REQUIRE( maxSiData[1] == Approx( 2.25F ) );
1441 REQUIRE( lpData[0] == Approx( 5.0F ) );
1442 REQUIRE( lpData[1] == Approx( 2.5F ) );
1443 REQUIRE( maxLpData[0] == Approx( 7.0F ) );
1444 REQUIRE( maxLpData[1] == Approx( 3.0F ) );
1445}
1446
1447TEST_CASE( "modalGainOpt zero_gains request resets the integrated SI gains", "[modalGainOpt]" )
1448{
1449 modalGainOptHarness app;
1450
1451 app.configurePublishedGainState( { 2.0F, 4.0F },
1452 { 29.0F, 31.0F },
1453 { 3.0F, 5.0F },
1454 { 7.0F, 11.0F },
1455 { 13.0F, 17.0F },
1456 { 19.0F, 23.0F },
1457 { 1.0F, 2.0F },
1458 { 0.1F, 0.2F },
1459 { 1.1F, 1.2F },
1460 { 2.1F, 2.2F },
1461 2.0F );
1462 app.initZeroGainsPropertyForTest();
1463
1464 REQUIRE( app.integratedSiGainsForTest()[0] == Approx( 3.0F ) );
1465 REQUIRE( app.integratedSiGainsForTest()[1] == Approx( 5.0F ) );
1466
1467 REQUIRE( app.requestZeroGainsForTest() == 0 );
1468 REQUIRE( app.integratedSiGainsForTest()[0] == Approx( 0.0F ) );
1469 REQUIRE( app.integratedSiGainsForTest()[1] == Approx( 0.0F ) );
1470}
1471
1472TEST_CASE( "modalGainOpt SI gain integrator updates toward the raw optimum by delta", "[modalGainOpt]" )
1473{
1474 modalGainOptHarness app;
1475
1476 app.configureSiIntegratorStateForTest( { 10.0F, 3.0F }, { 4.0F, 1.0F }, 0.2F, 0.9F );
1477
1478 app.updateIntegratedSiGainForTest( 0 );
1479 app.updateIntegratedSiGainForTest( 1 );
1480
1481 REQUIRE( app.integratedSiGainsForTest()[0] == Approx( 4.8F ) );
1482 REQUIRE( app.integratedSiGainsForTest()[1] == Approx( 1.3F ) );
1483}
1484
1485/// Verify `modalGainOpt` counts enabled modes from positive gain factors.
1486/**
1487 * \ingroup modalGainOpt_unit_test
1488 */
1489TEST_CASE( "modalGainOpt counts enabled gain-factor modes using positive entries", "[modalGainOpt]" )
1490{
1491 modalGainOptHarness app;
1492
1493 // clang-format off
1494 #ifdef MODALGAINOPT_TEST_DOXYGEN_REF
1495 modalGainOpt::countEnabledGainFactors( std::vector<float>() );
1496 #endif
1497 // clang-format on
1498
1499 REQUIRE( app.countEnabledGainFactorsForTest( {} ) == 0 );
1500 REQUIRE( app.countEnabledGainFactorsForTest( { -0.2F, 0.0F, 0.1F, 2.0F, -3.0F } ) == 2 );
1501 REQUIRE( app.countEnabledGainFactorsForTest( { 1.0F, 0.5F, 0.25F } ) == 3 );
1502}
1503
1504/// Verify `modalGainOpt` ignores unchanged gain-factor frames.
1505/**
1506 * \ingroup modalGainOpt_unit_test
1507 */
1508TEST_CASE( "modalGainOpt gain-factor updates leave state unchanged when the "
1509 "frame is identical",
1510 "[modalGainOpt]" )
1511{
1512 modalGainOptHarness app;
1513
1514 std::vector<float> storedGainFacts( { 1.0F, 0.5F, 0.0F } );
1515
1516 app.setLoopForTest( true );
1517 app.setPcOnForTest( false );
1518 app.setModesOnForTest( 7 );
1519 app.setSinceChangeForTest( 12 );
1520
1521 // clang-format off
1522 #ifdef MODALGAINOPT_TEST_DOXYGEN_REF
1523 modalGainOpt::applyGainFactorUpdate( storedGainFacts, static_cast<const float *>( nullptr ), 0, false );
1524 #endif
1525 // clang-format on
1526
1527 REQUIRE( app.applyGainFactorUpdateForTest( storedGainFacts, { 1.0F, 0.5F, 0.0F }, false ) == false );
1528 REQUIRE( storedGainFacts == std::vector<float>( { 1.0F, 0.5F, 0.0F } ) );
1529 REQUIRE( app.modesOn() == 7 );
1530 REQUIRE( app.sinceChange() == 12 );
1531}
1532
1533/// Verify `modalGainOpt` resizes and copies SI gain-factor frames while
1534/// resetting the loop debounce timer.
1535/**
1536 * \ingroup modalGainOpt_unit_test
1537 */
1538TEST_CASE( "modalGainOpt gain-factor updates resize SI state and reset "
1539 "sinceChange on change",
1540 "[modalGainOpt]" )
1541{
1542 modalGainOptHarness app;
1543
1544 std::vector<float> storedGainFacts( { 9.0F } );
1545
1546 app.setLoopForTest( true );
1547 app.setPcOnForTest( false );
1548 app.setModesOnForTest( 0 );
1549 app.setSinceChangeForTest( 5 );
1550
1551 REQUIRE( app.applyGainFactorUpdateForTest( storedGainFacts, { 0.0F, 1.5F, -2.0F, 3.0F }, false ) == true );
1552 REQUIRE( storedGainFacts == std::vector<float>( { 0.0F, 1.5F, -2.0F, 3.0F } ) );
1553 REQUIRE( app.modesOn() == 2 );
1554 REQUIRE( app.sinceChange() == -1 );
1555}
1556
1557/// Verify `modalGainOpt` ignores unchanged multiplier frames.
1558/**
1559 * \ingroup modalGainOpt_unit_test
1560 */
1561TEST_CASE( "modalGainOpt multiplier updates leave state unchanged when the "
1562 "frame is identical",
1563 "[modalGainOpt]" )
1564{
1565 modalGainOptHarness app;
1566
1567 std::vector<float> storedMultFacts( { 0.25F, 0.5F, 0.75F } );
1568
1569 app.setLoopForTest( true );
1570 app.setSinceChangeForTest( 14 );
1571 app.setGoptUpdatedForTest( false );
1572 app.setPcgoptUpdatedForTest( false );
1573
1574 // clang-format off
1575 #ifdef MODALGAINOPT_TEST_DOXYGEN_REF
1576 modalGainOpt::applyMultiplierUpdate( storedMultFacts, static_cast<const float *>( nullptr ), 0, false );
1577 #endif
1578 // clang-format on
1579
1580 REQUIRE( app.applyMultiplierUpdateForTest( storedMultFacts, { 0.25F, 0.5F, 0.75F }, false ) == false );
1581 REQUIRE( storedMultFacts == std::vector<float>( { 0.25F, 0.5F, 0.75F } ) );
1582 REQUIRE( app.sinceChange() == 14 );
1583 REQUIRE( app.goptUpdated() == false );
1584 REQUIRE( app.pcgoptUpdated() == false );
1585}
1586
1587/// Verify `modalGainOpt` marks SI optimizer state dirty when multiplier frames
1588/// change.
1589/**
1590 * \ingroup modalGainOpt_unit_test
1591 */
1592TEST_CASE( "modalGainOpt SI multiplier updates set goptUpdated and reset sinceChange", "[modalGainOpt]" )
1593{
1594 modalGainOptHarness app;
1595
1596 std::vector<float> storedMultFacts( { 1.0F } );
1597
1598 app.setLoopForTest( true );
1599 app.setSinceChangeForTest( 6 );
1600 app.setGoptUpdatedForTest( false );
1601 app.setPcgoptUpdatedForTest( false );
1602
1603 REQUIRE( app.applyMultiplierUpdateForTest( storedMultFacts, { 1.5F, 2.5F }, false ) == true );
1604 REQUIRE( storedMultFacts == std::vector<float>( { 1.5F, 2.5F } ) );
1605 REQUIRE( app.sinceChange() == -1 );
1606 REQUIRE( app.goptUpdated() == true );
1607 REQUIRE( app.pcgoptUpdated() == false );
1608}
1609
1610/// Verify `modalGainOpt` ignores unchanged frequency frames.
1611/**
1612 * \ingroup modalGainOpt_unit_test
1613 */
1614TEST_CASE( "modalGainOpt frequency updates leave state unchanged when the frame "
1615 "is identical",
1616 "[modalGainOpt]" )
1617{
1618 modalGainOptHarness app;
1619
1620 app.setFreqForTest( { 10.0F, 20.0F, 30.0F } );
1621 app.setFpsForTest( 60.0F );
1622 app.setSinceChangeForTest( 8 );
1623 app.setGoptUpdatedForTest( false );
1624 app.setFreqUpdatedForTest( false );
1625
1626 // clang-format off
1627 #ifdef MODALGAINOPT_TEST_DOXYGEN_REF
1628 modalGainOpt::applyFrequencyUpdate( static_cast<const float *>( nullptr ), 0 );
1629 #endif
1630 // clang-format on
1631
1632 REQUIRE( app.applyFrequencyUpdateForTest( { 10.0F, 20.0F, 30.0F } ) == false );
1633 REQUIRE( app.freq() == std::vector<float>( { 10.0F, 20.0F, 30.0F } ) );
1634 REQUIRE( app.fps() == Approx( 60.0F ) );
1635 REQUIRE( app.sinceChange() == 8 );
1636 REQUIRE( app.goptUpdated() == false );
1637 REQUIRE( app.freqUpdated() == false );
1638}
1639
1640/// Verify `modalGainOpt` resizes and copies changed frequency frames while
1641/// updating derived state.
1642/**
1643 * \ingroup modalGainOpt_unit_test
1644 */
1645TEST_CASE( "modalGainOpt frequency updates resize state and refresh derived timing", "[modalGainOpt]" )
1646{
1647 modalGainOptHarness app;
1648
1649 app.setFreqForTest( { 5.0F } );
1650 app.setFpsForTest( 10.0F );
1651 app.setSinceChangeForTest( 4 );
1652 app.setGoptUpdatedForTest( false );
1653 app.setFreqUpdatedForTest( false );
1654
1655 REQUIRE( app.applyFrequencyUpdateForTest( { 12.5F, 25.0F, 40.0F } ) == true );
1656 REQUIRE( app.freq() == std::vector<float>( { 12.5F, 25.0F, 40.0F } ) );
1657 REQUIRE( app.fps() == Approx( 80.0F ) );
1658 REQUIRE( app.sinceChange() == -1 );
1659 REQUIRE( app.goptUpdated() == true );
1660 REQUIRE( app.freqUpdated() == true );
1661}
1662
1663/// Verify `modalGainOpt` can refresh gain-optimization structures as soon as
1664/// metadata changes, without waiting for a PSD-triggered semaphore post.
1665/**
1666 * \ingroup modalGainOpt_unit_test
1667 */
1668TEST_CASE( "modalGainOpt refreshes pending gopt structures without a PSD wakeup", "[modalGainOpt]" )
1669{
1670 modalGainOptHarness app;
1671
1672 // clang-format off
1673 #ifdef MODALGAINOPT_TEST_DOXYGEN_REF
1675 #endif
1676 // clang-format on
1677
1678 app.setFpsForTest( 1000.0F );
1679 app.setFreqForTest( { 100.0F, 200.0F, 300.0F } );
1680 app.configureGoptStructureInputsForTest( { 1.0F, 2.0F }, { 0.001F, 0.002F }, { 0.5F, 0.75F }, app.freq() );
1681 app.setFreqUpdatedForTest( true );
1682 app.setGoptUpdatedForTest( true );
1683 app.setPcgoptUpdatedForTest( false );
1684 app.setPcOnForTest( false );
1685
1686 REQUIRE( app.goptCurrentSize() == 0 );
1687 REQUIRE( app.refreshGoptStructuresForTest() == true );
1688 REQUIRE( app.goptCurrentSize() == 2 );
1689 REQUIRE( app.goptUpdated() == false );
1690 REQUIRE( app.pcgoptUpdated() == false );
1691 REQUIRE( app.freqUpdated() == false );
1692}
1693
1694/// Verify `modalGainOpt` writes predictive-control coefficients into per-mode
1695/// blocks.
1696/**
1697 * \ingroup modalGainOpt_unit_test
1698 */
1699TEST_CASE( "modalGainOpt predictor publication preserves per-mode coefficient layout", "[modalGainOpt]" )
1700{
1701 modalGainOptHarness app;
1702
1703 app.configurePublishedPredictorState( { 2.0F, 4.0F },
1704 { 3.0F, 5.0F },
1705 { 1.0F, 2.0F },
1706 { 2U, 1U },
1707 { 1U, 2U },
1708 { { 0.1F, 0.2F }, { 0.4F } },
1709 { { 0.3F }, { 0.5F, 0.6F } },
1710 2.0F,
1711 0.5F );
1712
1713 std::vector<float> pcGainData( 2, -1.0F );
1714 std::vector<float> aCoeffData( 8, -1.0F );
1715 std::vector<float> bCoeffData( 8, -1.0F );
1716
1717 app.writePublishedPredictorArraysForTest( pcGainData.data(), aCoeffData.data(), 4, bCoeffData.data(), 4, false );
1718
1719 REQUIRE( pcGainData[0] == Approx( 3.0F ) );
1720 REQUIRE( pcGainData[1] == Approx( 5.0F ) );
1721
1722 REQUIRE( aCoeffData[0] == Approx( 2.0F ) );
1723 REQUIRE( aCoeffData[1] == Approx( 0.1F ) );
1724 REQUIRE( aCoeffData[2] == Approx( 0.2F ) );
1725 REQUIRE( aCoeffData[3] == Approx( 0.0F ) );
1726 REQUIRE( aCoeffData[4] == Approx( 1.0F ) );
1727 REQUIRE( aCoeffData[5] == Approx( 0.4F ) );
1728 REQUIRE( aCoeffData[6] == Approx( 0.0F ) );
1729 REQUIRE( aCoeffData[7] == Approx( 0.0F ) );
1730
1731 REQUIRE( bCoeffData[0] == Approx( 1.0F ) );
1732 REQUIRE( bCoeffData[1] == Approx( 0.3F ) );
1733 REQUIRE( bCoeffData[2] == Approx( 0.0F ) );
1734 REQUIRE( bCoeffData[3] == Approx( 0.0F ) );
1735 REQUIRE( bCoeffData[4] == Approx( 2.0F ) );
1736 REQUIRE( bCoeffData[5] == Approx( 0.5F ) );
1737 REQUIRE( bCoeffData[6] == Approx( 0.6F ) );
1738 REQUIRE( bCoeffData[7] == Approx( 0.0F ) );
1739}
1740
1741/// Verify `modalGainOpt` blends predictive-control gains and coefficients
1742/// against existing outputs.
1743/**
1744 * \ingroup modalGainOpt_unit_test
1745 */
1746TEST_CASE( "modalGainOpt predictor publication blends existing values and "
1747 "clears stale coefficients",
1748 "[modalGainOpt]" )
1749{
1750 modalGainOptHarness app;
1751
1752 app.configurePublishedPredictorState( { 2.0F, 4.0F },
1753 { 3.0F, 5.0F },
1754 { 1.0F, 2.0F },
1755 { 2U, 1U },
1756 { 1U, 2U },
1757 { { 0.1F, 0.5F }, { 0.9F } },
1758 { { 0.2F }, { 0.6F, 1.0F } },
1759 2.0F,
1760 0.25F );
1761
1762 std::vector<float> pcGainData( { 1.0F, 9.0F } );
1763 std::vector<float> aCoeffData( { 9.0F, 1.0F, 2.0F, 3.0F, 8.0F, 4.0F, 5.0F, 6.0F } );
1764 std::vector<float> bCoeffData( { 7.0F, 1.0F, 2.0F, 3.0F, 6.0F, 4.0F, 5.0F, 6.0F } );
1765
1766 app.writePublishedPredictorArraysForTest( pcGainData.data(), aCoeffData.data(), 4, bCoeffData.data(), 4, true );
1767
1768 REQUIRE( pcGainData[0] == Approx( 1.5F ) );
1769 REQUIRE( pcGainData[1] == Approx( 8.0F ) );
1770
1771 REQUIRE( aCoeffData[0] == Approx( 2.0F ) );
1772 REQUIRE( aCoeffData[1] == Approx( 0.775F ) );
1773 REQUIRE( aCoeffData[2] == Approx( 1.625F ) );
1774 REQUIRE( aCoeffData[3] == Approx( 0.0F ) );
1775 REQUIRE( aCoeffData[4] == Approx( 1.0F ) );
1776 REQUIRE( aCoeffData[5] == Approx( 3.225F ) );
1777 REQUIRE( aCoeffData[6] == Approx( 0.0F ) );
1778 REQUIRE( aCoeffData[7] == Approx( 0.0F ) );
1779
1780 REQUIRE( bCoeffData[0] == Approx( 1.0F ) );
1781 REQUIRE( bCoeffData[1] == Approx( 0.8F ) );
1782 REQUIRE( bCoeffData[2] == Approx( 0.0F ) );
1783 REQUIRE( bCoeffData[3] == Approx( 0.0F ) );
1784 REQUIRE( bCoeffData[4] == Approx( 2.0F ) );
1785 REQUIRE( bCoeffData[5] == Approx( 3.15F ) );
1786 REQUIRE( bCoeffData[6] == Approx( 4.0F ) );
1787 REQUIRE( bCoeffData[7] == Approx( 0.0F ) );
1788}
1789
1790/// Verify `modalGainOpt` only applies SI gain-factor mode counts when the SI
1791/// path is active.
1792/**
1793 * \ingroup modalGainOpt_unit_test
1794 */
1795TEST_CASE( "modalGainOpt SI mode counts update only while predictor control is off", "[modalGainOpt]" )
1796{
1797 modalGainOptHarness app;
1798
1799 // clang-format off
1800 #ifdef MODALGAINOPT_TEST_DOXYGEN_REF
1801 modalGainOpt::updateAppliedModeCount( std::vector<float>(), false );
1802 #endif
1803 // clang-format on
1804
1805 app.setPcOnForTest( false );
1806 app.updateAppliedModeCountForTest( { 1.0F, 0.0F, -1.0F, 2.0F }, false );
1807 REQUIRE( app.modesOn() == 2 );
1808
1809 app.setPcOnForTest( true );
1810 app.updateAppliedModeCountForTest( { 5.0F, 4.0F, 3.0F }, false );
1811 REQUIRE( app.modesOn() == 2 );
1812}
1813
1814/// Verify `modalGainOpt` clears stale predictor coefficients when a mode
1815/// publishes zero-order predictors.
1816/**
1817 * \ingroup modalGainOpt_unit_test
1818 */
1819TEST_CASE( "modalGainOpt predictor publication clears stale coefficient blocks "
1820 "for zero-order modes",
1821 "[modalGainOpt]" )
1822{
1823 modalGainOptHarness app;
1824
1825 app.configurePublishedPredictorState( { 8.0F, 2.0F },
1826 { 4.0F, 6.0F },
1827 { 4.0F, 2.0F },
1828 { 0U, 0U },
1829 { 0U, 0U },
1830 { {}, {} },
1831 { {}, {} },
1832 2.0F,
1833 0.5F );
1834
1835 std::vector<float> pcGainData( { 9.0F, 10.0F } );
1836 std::vector<float> aCoeffData( { 7.0F, 1.0F, 2.0F, 3.0F, 6.0F, 4.0F, 5.0F, 6.0F } );
1837 std::vector<float> bCoeffData( { 5.0F, 7.0F, 8.0F, 9.0F, 4.0F, 10.0F, 11.0F, 12.0F } );
1838
1839 app.writePublishedPredictorArraysForTest( pcGainData.data(), aCoeffData.data(), 4, bCoeffData.data(), 4, false );
1840
1841 REQUIRE( pcGainData[0] == Approx( 4.0F ) );
1842 REQUIRE( pcGainData[1] == Approx( 3.0F ) );
1843
1844 REQUIRE( aCoeffData[0] == Approx( 0.0F ) );
1845 REQUIRE( aCoeffData[1] == Approx( 0.0F ) );
1846 REQUIRE( aCoeffData[2] == Approx( 0.0F ) );
1847 REQUIRE( aCoeffData[3] == Approx( 0.0F ) );
1848 REQUIRE( aCoeffData[4] == Approx( 0.0F ) );
1849 REQUIRE( aCoeffData[5] == Approx( 0.0F ) );
1850 REQUIRE( aCoeffData[6] == Approx( 0.0F ) );
1851 REQUIRE( aCoeffData[7] == Approx( 0.0F ) );
1852
1853 REQUIRE( bCoeffData[0] == Approx( 0.0F ) );
1854 REQUIRE( bCoeffData[1] == Approx( 0.0F ) );
1855 REQUIRE( bCoeffData[2] == Approx( 0.0F ) );
1856 REQUIRE( bCoeffData[3] == Approx( 0.0F ) );
1857 REQUIRE( bCoeffData[4] == Approx( 0.0F ) );
1858 REQUIRE( bCoeffData[5] == Approx( 0.0F ) );
1859 REQUIRE( bCoeffData[6] == Approx( 0.0F ) );
1860 REQUIRE( bCoeffData[7] == Approx( 0.0F ) );
1861}
1862
1863/// Verify `modalGainOpt` only applies PC gain-factor mode counts when predictor
1864/// control is on.
1865/**
1866 * \ingroup modalGainOpt_unit_test
1867 */
1868TEST_CASE( "modalGainOpt PC mode counts update only while predictor control is on", "[modalGainOpt]" )
1869{
1870 modalGainOptHarness app;
1871
1872 app.setPcOnForTest( false );
1873 app.updateAppliedModeCountForTest( { 1.0F, 2.0F, 3.0F }, true );
1874 REQUIRE( app.modesOn() == 0 );
1875
1876 app.setPcOnForTest( true );
1877 app.updateAppliedModeCountForTest( { -1.0F, 0.25F, 0.0F, 0.75F }, true );
1878 REQUIRE( app.modesOn() == 2 );
1879}
1880
1881/// Verify `modalGainOpt` updates stored PC gain factors without disturbing
1882/// SI-applied mode counts when predictor control is off.
1883/**
1884 * \ingroup modalGainOpt_unit_test
1885 */
1886TEST_CASE( "modalGainOpt PC gain-factor updates preserve applied mode counts "
1887 "while predictor control is off",
1888 "[modalGainOpt]" )
1889{
1890 modalGainOptHarness app;
1891
1892 std::vector<float> storedPcGainFacts( { 0.5F, 0.5F } );
1893
1894 app.setLoopForTest( false );
1895 app.setPcOnForTest( false );
1896 app.setModesOnForTest( 3 );
1897 app.setSinceChangeForTest( 9 );
1898
1899 REQUIRE( app.applyGainFactorUpdateForTest( storedPcGainFacts, { 1.0F, 0.0F, 2.0F }, true ) == true );
1900 REQUIRE( storedPcGainFacts == std::vector<float>( { 1.0F, 0.0F, 2.0F } ) );
1901 REQUIRE( app.modesOn() == 3 );
1902 REQUIRE( app.sinceChange() == 9 );
1903}
1904
1905/// Verify `modalGainOpt` marks predictive-control optimizer state dirty when PC
1906/// multiplier frames change.
1907/**
1908 * \ingroup modalGainOpt_unit_test
1909 */
1910TEST_CASE( "modalGainOpt PC multiplier updates set pcgoptUpdated without "
1911 "touching SI optimizer flags",
1912 "[modalGainOpt]" )
1913{
1914 modalGainOptHarness app;
1915
1916 std::vector<float> storedPcMultFacts( { 0.1F, 0.2F } );
1917
1918 app.setLoopForTest( false );
1919 app.setSinceChangeForTest( 11 );
1920 app.setGoptUpdatedForTest( false );
1921 app.setPcgoptUpdatedForTest( false );
1922
1923 REQUIRE( app.applyMultiplierUpdateForTest( storedPcMultFacts, { 0.3F, 0.4F, 0.5F }, true ) == true );
1924 REQUIRE( storedPcMultFacts == std::vector<float>( { 0.3F, 0.4F, 0.5F } ) );
1925 REQUIRE( app.sinceChange() == 11 );
1926 REQUIRE( app.goptUpdated() == false );
1927 REQUIRE( app.pcgoptUpdated() == true );
1928}
1929
1930} // namespace modalGainOptTest
1931
1932} // namespace libXWCTest
The MagAO-X PSD-based gain optimizer.
int countEnabledGainFactors(const std::vector< float > &gainFacts) const
Count how many modes are enabled by a gain-factor vector.
void updateAppliedModeCount(const std::vector< float > &gainFacts, bool predictorPath)
bool applyMultiplierUpdate(std::vector< float > &multFacts, const float *incoming, uint32_t width, bool predictorPath)
bool applyFrequencyUpdate(const float *incoming, size_t size)
Apply an incoming frequency frame to the stored frequency scale.
bool applyGainFactorUpdate(std::vector< float > &gainFacts, const float *incoming, uint32_t width, bool predictorPath)
Apply an incoming gain-factor frame to one of the stored gain vectors.
std::string m_powerLawCrossoverMode
How the power-law match/cutoff frequencies were chosen.
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.
size_t m_powerLawFitBinsUsed
The number of populated median bins used in the exponent fit.
realT m_noiseFloor
The fitted flat noise floor.
std::vector< identifiedPeak1D > m_peaks
The peaks detected by the Moffat extrapolator.
static mx::error_t buildSmoothedProcessPsd(std::vector< realT > &smoothedProcessPsd, const std::vector< realT > &rawProcessPsd, const std::vector< realT > &freq, realT smoothWidthHz)
static mx::error_t findAutoPowerLawCrossoverFreq(realT &crossoverFreq, const std::vector< realT > &smoothedProcessPsd, const std::vector< realT > &noisePsd, const std::vector< realT > &freq, realT maxFreqFraction)
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::vector< realT > m_processPsd
The disturbance PSD used for optimization.
realT m_powerLawOnlyAboveFreq
Above this frequency, force the extrapolation to be power-law only.
realT m_extrapolation
The continuum normalization at m_powerLawNormFreq.
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 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.
realT m_powerLawIndex
The power-law exponent used in extrapolation.
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 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.
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 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.
bool m_powerLawIndexFitSucceeded
Whether the exponent fit succeeded and was applied.
realT m_powerLawFitMinFreqHz
The low edge of the exponent-fit range.
std::string m_method
The extrapolation method name.
realT m_powerLawFitBinWidthHz
The width of the exponent-fit median bins.
Configuration of the disturbance-PSD extrapolation model.
Results of modal PSD noise estimation and disturbance extrapolation.
TEST_CASE("modalGainOpt placeholder harness instantiates the app", "[modalGainOpt]")
int extrapClosedLoopOlEstimateMethodFromElement(const std::string &element)
std::string extrapClosedLoopOlEstimateMethodLabel(int method)
return handleExtrapNoiseEstimateStatisticProperty(ipRecv)
int olProcessMethodFromName(std::string method)
const pcf::IndiProperty & ipRecv
return handleExtrapNoiseEstimateRangeProperty(ipRecv)
return handleExtrapClosedLoopOlEstimateMethodProperty(ipRecv)
int olProcessMethodFromElement(const std::string &element)
std::string extrapNoiseEstimateDomainElement(int domain)
static constexpr int c_olProcessPowerLawOnly
std::string extrapNoiseEstimateRangeLabel(int range)
std::string extrapNoiseEstimateRangeName(int range)
std::string extrapClosedLoopOlEstimateMethodElement(int method)
std::string extrapNoiseEstimateStatisticElement(int statistic)
std::string extrapPowerLawCrossoverModeElement(int mode)
std::string extrapNoiseEstimateRangeElement(int range)
static constexpr int c_olProcessLegacy
return handleExtrapNoiseEstimateDomainProperty(ipRecv)
static constexpr int c_extrapNoiseEstimateHighFreq
std::string extrapNoiseEstimateDomainName(int domain)
static constexpr int c_extrapPowerLawCrossoverManual
std::string extrapPowerLawCrossoverModeName(int mode)
static constexpr int c_olProcessNone
static constexpr int c_extrapNoiseEstimateMinimum
static constexpr int c_olProcessMoffatPeaks
return handleExtrapPowerLawCrossoverModeProperty(ipRecv)
static constexpr int c_extrapNoiseEstimatePercentile
static constexpr int c_extrapClosedLoopOlEstimateEtfOnly
std::string olProcessMethodElement(int method)
std::string extrapClosedLoopOlEstimateMethodName(int method)
static constexpr int c_extrapNoiseEstimateLowFreq
std::string extrapNoiseEstimateStatisticName(int statistic)
int extrapNoiseEstimateDomainFromElement(const std::string &element)
std::string extrapNoiseEstimateStatisticLabel(int statistic)
int extrapNoiseEstimateStatisticFromElement(const std::string &element)
int extrapNoiseEstimateRangeFromElement(const std::string &element)
std::string extrapPowerLawCrossoverModeLabel(int mode)
static constexpr int c_extrapNoiseEstimateOpenLoop
static constexpr int c_extrapNoiseEstimateClosedLoopPreXfer
static constexpr int c_extrapPowerLawCrossoverAutoSmoothedCrossing
int extrapClosedLoopOlEstimateMethodFromName(std::string method)
std::unique_lock< std::mutex > lock(m_indiMutex)
int extrapNoiseEstimateStatisticFromName(std::string statistic)
int extrapNoiseEstimateDomainFromName(std::string domain)
static constexpr int c_extrapClosedLoopOlEstimateNtfAware
std::string extrapNoiseEstimateDomainLabel(int domain)
std::string olProcessMethodLabel(int method)
std::string olProcessMethodName(int method)
int extrapNoiseEstimateRangeFromName(std::string range)
return handleExtrapMethodProperty(ipRecv)
Namespace for all libXWC tests.