API
 
Loading...
Searching...
No Matches
modalPSDs_test.cpp
Go to the documentation of this file.
1/** \file modalPSDs_test.cpp
2 * \brief Catch2 tests for the modalPSDs app.
3 * \author Jared R. Males (jaredmales@gmail.com)
4 *
5 * \ingroup modalPSDs_files
6 */
7
8#include "../../../tests/testXWC.hpp"
9#include "../../../tests/testMacrosINDI.hpp"
10
11#include "../modalPSDs.hpp"
12
13using namespace MagAOX::app;
14
15namespace libXWCTest
16{
17
18/** \defgroup modalPSDs_unit_test modalPSDs Unit Tests
19 * \brief Unit tests for the modalPSDs application.
20 *
21 * \ingroup application_unit_test
22 */
23
24/// Namespace for `modalPSDs` unit tests.
25/** \ingroup modalPSDs_unit_test
26 */
27namespace modalPSDsTest
28{
29
30/// \cond DOXYGEN_SUPPRESS_TEST_HARNESS
31class modalPSDs_test : public modalPSDs
32{
33
34 public:
35 using snapshotT = ampCircBuffT::snapshotT;
36
37 modalPSDs_test( const std::string &device )
38 {
39 m_configName = device;
40
42 XWCTEST_SETUP_INDI_NEW_PROP( psdAvgTime );
44 XWCTEST_SETUP_INDI_ARB_PROP( m_indiP_fpsSource, modeamps, fps );
45 XWCTEST_SETUP_INDI_ARB_PROP( m_indiP_loop, loopdev, loop_state );
46 }
47
48 void setWindowSizes( cbIndexT tsSize, cbIndexT meanSize )
49 {
50 m_tsSize = tsSize;
51 m_meanSize = meanSize;
52 m_tsPtrs.resize( m_tsSize );
53 m_meanPtrs.resize( m_meanSize );
54 }
55
56 void setModeCount( size_t nModes )
57 {
58 m_nModes = nModes;
59 }
60
61 void setCircBuffEntries( cbIndexT entries )
62 {
63 m_ampCircBuff.maxEntries( entries );
64 }
65
66 cbIndexT circBuffSize() const
67 {
68 return m_ampCircBuff.size();
69 }
70
71 void pushSample( realT *ptr )
72 {
73 m_ampCircBuff.nextEntry( ptr );
74 }
75
76 int processImageForTest( realT *ptr )
77 {
78 dev::shmimT dummy;
79 return processImage( ptr, dummy );
80 }
81
82 bool loadWindows( snapshotT &sn )
83 {
84 return loadPsdInputWindows( sn );
85 }
86
87 realT tsValue( size_t n ) const
88 {
89 return *( m_tsPtrs.at( n ) );
90 }
91
92 realT meanValue( size_t n ) const
93 {
94 return *( m_meanPtrs.at( n ) );
95 }
96
97 cbIndexT latestRef( const snapshotT &sn, cbIndexT count ) const
98 {
99 return latestWindowRefEntry( sn, count );
100 }
101
102 cbIndexT precedingRef( const snapshotT &sn, cbIndexT refEntry, cbIndexT count ) const
103 {
104 return precedingWindowRefEntry( sn, refEntry, count );
105 }
106
107 void setPSDTiming( realT psdTime, realT psdAvgTime, realT meanTime, realT psdOverlapFraction )
108 {
109 m_psdTime.store( psdTime );
110 m_psdAvgTime.store( psdAvgTime );
111 m_meanTime.store( meanTime );
112 m_psdOverlapFraction = psdOverlapFraction;
113 }
114
115 void setPSDHistoryFloor( int nPSDHistory )
116 {
117 m_nPSDHistory = nPSDHistory;
118 }
119
120 void
121 configureLoopState( const std::string &device, const std::string &property, const std::string &element = "toggle" )
122 {
123 m_loopStateDevice = device;
124 m_loopStateProperty = property;
125 m_loopStateElement = element;
126 m_useLoopState = !m_loopStateDevice.empty();
127 m_loopClosed.store( m_useLoopState == false, std::memory_order_release );
128
129 m_indiP_loop.setDevice( device );
130 m_indiP_loop.setName( property );
131 }
132
133 void setLoopClosedForTest( bool loopClosed )
134 {
135 m_loopClosed.store( loopClosed, std::memory_order_release );
136 }
137
138 bool acceptLoopStateFrameForTest() const
139 {
140 return acceptLoopStateFrame();
141 }
142
143 bool usesLoopStateForTest() const
144 {
145 return m_useLoopState;
146 }
147
148 bool loopClosedForTest() const
149 {
150 return m_loopClosed.load( std::memory_order_acquire );
151 }
152
153 int desiredPSDAverageCountForTest() const
154 {
155 return desiredPSDAverageCount();
156 }
157
158 cbIndexT desiredMeanSampleCountForTest( realT fps ) const
159 {
160 return desiredMeanSampleCount( fps );
161 }
162
163 cbIndexT requiredInputHistoryDepthForTest()
164 {
165 return requiredInputHistoryDepth();
166 }
167
168 static cbIndexT circularEntryAdvanceForTest( cbIndexT from, cbIndexT to, cbIndexT maxEntries )
169 {
170 return circularEntryAdvance( from, to, maxEntries );
171 }
172
173 std::vector<double> recomputeMeanSumsForTest() const
174 {
175 std::vector<double> meanSums;
176 recomputeMeanSums( meanSums );
177 return meanSums;
178 }
179
180 std::vector<realT> cacheMeanHeadForTest( cbIndexT count ) const
181 {
182 std::vector<realT> meanHeadCache;
183 cacheMeanHead( meanHeadCache, count );
184 return meanHeadCache;
185 }
186
187 void rollMeanSumsForTest( std::vector<double> &meanSums,
188 const std::vector<realT> &meanHeadCache,
189 cbIndexT advance ) const
190 {
191 rollMeanSums( meanSums, meanHeadCache, advance );
192 }
193
194 static void updatePlaneSumForTest( std::vector<double> &planeSum,
195 const std::vector<realT> &addPlane,
196 const std::vector<realT> *removePlane = nullptr )
197 {
198 updatePlaneSum(
199 planeSum, addPlane.data(), removePlane == nullptr ? nullptr : removePlane->data(), addPlane.size() );
200 }
201
202 uint32_t rawPSDHistoryDepthForTest() const
203 {
204 return rawPSDHistoryDepth();
205 }
206
207 uint32_t publishedRawPSDHistoryDepthForTest() const
208 {
209 return publishedRawPSDHistoryDepth();
210 }
211};
212/// \endcond
213
214/// Verify modalPSDs callback validation and PSD-window extraction logic behave as expected.
215/**
216 * \ingroup modalPSDs_unit_test
217 */
218SCENARIO( "INDI Callbacks", "[modalPSDs]" )
219{
220 // clang-format off
221 #ifdef MODALPSDS_TEST_DOXYGEN_REF
222 modalPSDs::newCallBack_m_indiP_psdTime( pcf::IndiProperty() );
223 modalPSDs::newCallBack_m_indiP_psdAvgTime( pcf::IndiProperty() );
224 modalPSDs::newCallBack_m_indiP_meanTime( pcf::IndiProperty() );
225 modalPSDs::setCallBack_m_indiP_fpsSource( pcf::IndiProperty() );
226 modalPSDs::loadPsdInputWindows( *(ampCircBuffT::snapshotT *)nullptr );
227 #endif
228 // clang-format on
229
233 XWCTEST_INDI_SET_CALLBACK( modalPSDs, m_indiP_fpsSource, modeamps, fps );
234 XWCTEST_INDI_SET_CALLBACK( modalPSDs, m_indiP_loop, loopdev, loop_state );
235}
236
237/// Verify modalPSDs derives PSD averaging depth from psdTime and psdAvgTime while mean sizing follows meanTime.
238/**
239 * \ingroup modalPSDs_unit_test
240 */
241TEST_CASE( "modalPSDs PSD averaging and mean windows are decoupled", "[modalPSDs]" )
242{
243 modalPSDs_test app( "modalPSDs_test" );
244
245 // clang-format off
246 #ifdef MODALPSDS_TEST_DOXYGEN_REF
251 #endif
252 // clang-format on
253
254 app.setPSDHistoryFloor( 100 );
255
256 app.setPSDTiming( 1.0F, 10.0F, 60.0F, 0.5F );
257 app.setWindowSizes( 2000, app.desiredMeanSampleCountForTest( 1000.0F ) );
258 REQUIRE( app.desiredPSDAverageCountForTest() == 20 );
259 REQUIRE( app.desiredMeanSampleCountForTest( 1000.0F ) == 60000 );
260 REQUIRE( app.requiredInputHistoryDepthForTest() == 62002 );
261 REQUIRE( app.rawPSDHistoryDepthForTest() == 0 );
262 REQUIRE( app.publishedRawPSDHistoryDepthForTest() == 100 );
263
264 app.setPSDTiming( 1.0F, 60.0F, 60.0F, 0.5F );
265 app.setWindowSizes( 2000, app.desiredMeanSampleCountForTest( 1000.0F ) );
266 REQUIRE( app.desiredPSDAverageCountForTest() == 120 );
267 REQUIRE( app.desiredMeanSampleCountForTest( 1000.0F ) == 60000 );
268 REQUIRE( app.requiredInputHistoryDepthForTest() == 62002 );
269 REQUIRE( app.rawPSDHistoryDepthForTest() == 21 );
270 REQUIRE( app.publishedRawPSDHistoryDepthForTest() == 100 );
271
272 app.setPSDTiming( 1.0F, 60.0F, 15.0F, 0.5F );
273 app.setWindowSizes( 2000, app.desiredMeanSampleCountForTest( 1000.0F ) );
274 REQUIRE( app.desiredPSDAverageCountForTest() == 120 );
275 REQUIRE( app.desiredMeanSampleCountForTest( 1000.0F ) == 15000 );
276 REQUIRE( app.requiredInputHistoryDepthForTest() == 17002 );
277 REQUIRE( app.rawPSDHistoryDepthForTest() == 21 );
278}
279
280SCENARIO( "PSD input windows come from one validated snapshot", "[modalPSDs]" )
281{
282 GIVEN( "A full fixed-size circular buffer and configured PSD window lengths" )
283 {
284 modalPSDs_test app( "modalPSDs_test" );
285 app.setWindowSizes( 3, 2 );
286 app.setCircBuffEntries( 8 );
287
288 modalPSDs::realT samples[9];
289 for( int n = 0; n < 9; ++n )
290 {
291 samples[n] = static_cast<modalPSDs::realT>( n );
292 }
293
294 for( int n = 0; n < 8; ++n )
295 {
296 app.pushSample( &samples[n] );
297 }
298
299 WHEN( "The PSD and mean windows are loaded before wraparound" )
300 {
301 modalPSDs_test::snapshotT sn;
302
303 REQUIRE( app.loadWindows( sn ) );
304 REQUIRE( sn.maxEntries == 8 );
305 REQUIRE( sn.validEntries == 8 );
306
307 REQUIRE( app.tsValue( 0 ) == 4 );
308 REQUIRE( app.tsValue( 1 ) == 5 );
309 REQUIRE( app.tsValue( 2 ) == 6 );
310
311 REQUIRE( app.meanValue( 0 ) == 2 );
312 REQUIRE( app.meanValue( 1 ) == 3 );
313 }
314
315 WHEN( "The buffer has wrapped once" )
316 {
317 app.pushSample( &samples[8] );
318
319 modalPSDs_test::snapshotT sn;
320
321 REQUIRE( app.loadWindows( sn ) );
322 REQUIRE( sn.latest == 0 );
323 REQUIRE( sn.earliest == 1 );
324
325 REQUIRE( app.tsValue( 0 ) == 5 );
326 REQUIRE( app.tsValue( 1 ) == 6 );
327 REQUIRE( app.tsValue( 2 ) == 7 );
328
329 REQUIRE( app.meanValue( 0 ) == 3 );
330 REQUIRE( app.meanValue( 1 ) == 4 );
331 }
332 }
333}
334
335/// Verify modalPSDs can read both windows when the circular buffer is sized to the exact required history depth.
336/**
337 * \ingroup modalPSDs_unit_test
338 */
339TEST_CASE( "modalPSDs PSD input windows use the exact required history depth", "[modalPSDs]" )
340{
341 modalPSDs_test app( "modalPSDs_test_required_history_depth" );
342
343 app.setWindowSizes( 3, 2 );
344 app.setCircBuffEntries( 7 );
345
346 modalPSDs::realT samples[7];
347 for( int n = 0; n < 7; ++n )
348 {
349 samples[n] = static_cast<modalPSDs::realT>( n );
350 app.pushSample( &samples[n] );
351 }
352
353 modalPSDs_test::snapshotT sn;
354
355 REQUIRE( app.loadWindows( sn ) );
356 REQUIRE( sn.maxEntries == 7 );
357 REQUIRE( sn.validEntries == 7 );
358
359 REQUIRE( app.meanValue( 0 ) == 1 );
360 REQUIRE( app.meanValue( 1 ) == 2 );
361
362 REQUIRE( app.tsValue( 0 ) == 3 );
363 REQUIRE( app.tsValue( 1 ) == 4 );
364 REQUIRE( app.tsValue( 2 ) == 5 );
365}
366
367/// Verify modalPSDs rolling mean updates match a full recomputation after one overlap advance.
368/**
369 * \ingroup modalPSDs_unit_test
370 */
371TEST_CASE( "modalPSDs rolling mean update matches full recompute", "[modalPSDs]" )
372{
373 modalPSDs_test app( "modalPSDs_test_rolling_mean" );
374
375 // clang-format off
376 #ifdef MODALPSDS_TEST_DOXYGEN_REF
378 modalPSDs::recomputeMeanSums( *(std::vector<double> *)nullptr );
379 modalPSDs::rollMeanSums( *(std::vector<double> *)nullptr, *(std::vector<modalPSDs::realT> *)nullptr, 0 );
380 modalPSDs::cacheMeanHead( *(std::vector<modalPSDs::realT> *)nullptr, 0 );
381 #endif
382 // clang-format on
383
384 app.setModeCount( 2 );
385 app.setWindowSizes( 4, 4 );
386 app.setCircBuffEntries( 10 );
387
388 modalPSDs::realT samples[12][2];
389 for( int n = 0; n < 12; ++n )
390 {
391 samples[n][0] = static_cast<modalPSDs::realT>( n );
392 samples[n][1] = static_cast<modalPSDs::realT>( 100 + 2 * n );
393 }
394
395 for( int n = 0; n < 10; ++n )
396 {
397 app.pushSample( samples[n] );
398 }
399
400 modalPSDs_test::snapshotT firstSnap;
401 REQUIRE( app.loadWindows( firstSnap ) );
402
403 auto firstSums = app.recomputeMeanSumsForTest();
404 auto firstHead = app.cacheMeanHeadForTest( 2 );
405
406 modalPSDs::cbIndexT firstTsRef = app.latestRef( firstSnap, 4 );
407 modalPSDs::cbIndexT firstMeanRef = app.precedingRef( firstSnap, firstTsRef, 4 );
408
409 app.pushSample( samples[10] );
410 app.pushSample( samples[11] );
411
412 modalPSDs_test::snapshotT secondSnap;
413 REQUIRE( app.loadWindows( secondSnap ) );
414
415 modalPSDs::cbIndexT secondTsRef = app.latestRef( secondSnap, 4 );
416 modalPSDs::cbIndexT secondMeanRef = app.precedingRef( secondSnap, secondTsRef, 4 );
417
418 REQUIRE( modalPSDs_test::circularEntryAdvanceForTest( firstMeanRef, secondMeanRef, secondSnap.maxEntries ) == 2 );
419
420 auto rolledSums = firstSums;
421 app.rollMeanSumsForTest( rolledSums, firstHead, 2 );
422
423 auto recomputedSums = app.recomputeMeanSumsForTest();
424
425 REQUIRE( rolledSums.size() == recomputedSums.size() );
426 for( size_t n = 0; n < rolledSums.size(); ++n )
427 {
428 REQUIRE( rolledSums[n] == Approx( recomputedSums[n] ) );
429 }
430}
431
432/// Verify modalPSDs rolling PSD-sum updates match a full recomputation when one plane enters and one leaves.
433/**
434 * \ingroup modalPSDs_unit_test
435 */
436TEST_CASE( "modalPSDs rolling PSD sum update matches full recompute", "[modalPSDs]" )
437{
438 // clang-format off
439 #ifdef MODALPSDS_TEST_DOXYGEN_REF
440 modalPSDs::updatePlaneSum( *(std::vector<double> *)nullptr, (const modalPSDs::realT *)nullptr, (const modalPSDs::realT *)nullptr, 0 );
441 #endif
442 // clang-format on
443
444 std::vector<double> rollingSum{ 12.0, 15.0, 18.0 };
445 std::vector<double> recomputedSum( 3, 0.0 );
446 std::vector<modalPSDs::realT> oldPlane{ 1.0F, 2.0F, 3.0F };
447 std::vector<modalPSDs::realT> keepPlaneA{ 4.0F, 5.0F, 6.0F };
448 std::vector<modalPSDs::realT> keepPlaneB{ 7.0F, 8.0F, 9.0F };
449 std::vector<modalPSDs::realT> newPlane{ 10.0F, 11.0F, 12.0F };
450
451 modalPSDs_test::updatePlaneSumForTest( rollingSum, newPlane, &oldPlane );
452
453 modalPSDs_test::updatePlaneSumForTest( recomputedSum, keepPlaneA );
454 modalPSDs_test::updatePlaneSumForTest( recomputedSum, keepPlaneB );
455 modalPSDs_test::updatePlaneSumForTest( recomputedSum, newPlane );
456
457 REQUIRE( rollingSum.size() == recomputedSum.size() );
458 for( size_t n = 0; n < rollingSum.size(); ++n )
459 {
460 REQUIRE( rollingSum[n] == Approx( recomputedSum[n] ) );
461 }
462}
463
464/// Verify modalPSDs optionally gates PSD ingestion on the configured loop-state property.
465/**
466 * \ingroup modalPSDs_unit_test
467 */
468TEST_CASE( "modalPSDs loop-state gating is optional and blocks open-loop frames", "[modalPSDs]" )
469{
470 modalPSDs_test app( "modalPSDs_test_loop_state" );
471
472 // clang-format off
473 #ifdef MODALPSDS_TEST_DOXYGEN_REF
475 modalPSDs::setCallBack_m_indiP_loop( pcf::IndiProperty() );
476 #endif
477 // clang-format on
478
479 modalPSDs::realT sample[1] = { 1.0F };
480
481 app.setCircBuffEntries( 4 );
482 REQUIRE( app.usesLoopStateForTest() == false );
483 REQUIRE( app.acceptLoopStateFrameForTest() == true );
484 REQUIRE( app.processImageForTest( sample ) == 0 );
485 REQUIRE( app.circBuffSize() == 1 );
486
487 app.setCircBuffEntries( 4 );
488 app.configureLoopState( "loopdev", "loop_state" );
489 REQUIRE( app.usesLoopStateForTest() == true );
490 REQUIRE( app.loopClosedForTest() == false );
491 REQUIRE( app.acceptLoopStateFrameForTest() == false );
492 REQUIRE( app.processImageForTest( sample ) == 0 );
493 REQUIRE( app.circBuffSize() == 0 );
494
495 app.setLoopClosedForTest( true );
496 REQUIRE( app.loopClosedForTest() == true );
497 REQUIRE( app.acceptLoopStateFrameForTest() == true );
498 REQUIRE( app.processImageForTest( sample ) == 0 );
499 REQUIRE( app.circBuffSize() == 1 );
500}
501
502SCENARIO( "Snapshot-based circular-buffer loads reject stale snapshots", "[modalPSDs]" )
503{
504 GIVEN( "A full fixed-size circular buffer" )
505 {
507 buff.maxEntries( 4 );
508
509 modalPSDs::realT samples[5];
510 for( int n = 0; n < 5; ++n )
511 {
512 samples[n] = static_cast<modalPSDs::realT>( n );
513 }
514
515 for( int n = 0; n < 4; ++n )
516 {
517 buff.nextEntry( &samples[n] );
518 }
519
520 modalPSDs::realT *dest[2] = { nullptr, nullptr };
521
522 WHEN( "The producer has not advanced" )
523 {
524 auto sn = buff.snapshot();
525
526 REQUIRE( buff.loadSequence( sn, 1, 2, dest ) );
527 REQUIRE( *dest[0] == 1 );
528 REQUIRE( *dest[1] == 2 );
529 }
530
531 WHEN( "The producer advances after the snapshot" )
532 {
533 auto sn = buff.snapshot();
534 buff.nextEntry( &samples[4] );
535
536 REQUIRE_FALSE( buff.loadSequence( sn, 1, 2, dest ) );
537 }
538 }
539}
540
541//} //namespace modalPSDs_test
542
543} // namespace modalPSDsTest
544
545} // namespace libXWCTest
Class for application to calculate rolling PSDs of modal amplitudes.
Definition modalPSDs.hpp:44
void cacheMeanHead(std::vector< realT > &meanHeadCache, cbIndexT count) const
Cache the oldest slice of the currently loaded mean window for the next rolling-mean update.
void recomputeMeanSums(std::vector< double > &meanSums) const
Recompute the per-mode sums for the full mean window from the currently loaded pointers.
cbIndexT desiredMeanSampleCount(realT fps) const
Calculate how many samples are needed for the mean-subtraction window at the current FPS.
cbIndexT requiredInputHistoryDepth() const
Calculate the total input-history depth needed to read both windows safely from the fixed-size circul...
void rollMeanSums(std::vector< double > &meanSums, const std::vector< realT > &meanHeadCache, cbIndexT advance) const
Update per-mode mean sums using the cached oldest slice and the newest loaded mean-window slice.
mx::sigproc::circularBufferIndex< realT *, cbIndexT, true > ampCircBuffT
The amplitude circular buffer type.
Definition modalPSDs.hpp:61
static void updatePlaneSum(std::vector< double > &planeSum, const realT *addPlane, const realT *removePlane, size_t planeElements)
Add one raw PSD plane and optionally subtract one outgoing raw PSD plane from the running average sum...
bool acceptLoopStateFrame() const
Determine whether incoming frames should currently be accepted into the PSD history.
int32_t cbIndexT
The index for the circular buffer.
Definition modalPSDs.hpp:55
int desiredPSDAverageCount() const
Calculate how many raw PSD estimates are needed to cover the requested averaging time.
bool loadPsdInputWindows(ampCircBuffT::snapshotT &sn)
Load the PSD and mean pointer windows from a single validated snapshot.
static cbIndexT circularEntryAdvance(cbIndexT from, cbIndexT to, cbIndexT maxEntries)
Compute the forward logical advance between two circular-buffer reference entries.
uint32_t rawPSDHistoryDepth() const
Calculate the additional PSD history depth needed beyond the published raw-PSD shmim.
TEST_CASE("modalPSDs PSD averaging and mean windows are decoupled", "[modalPSDs]")
Verify modalPSDs derives PSD averaging depth from psdTime and psdAvgTime while mean sizing follows me...
SCENARIO("INDI Callbacks", "[modalPSDs]")
Verify modalPSDs callback validation and PSD-window extraction logic behave as expected.
#define XWCTEST_INDI_SET_CALLBACK(testclass, varname, device, propname)
Catch-2 tests for whether a SET callback properly validates the input property properly.
#define XWCTEST_INDI_NEW_CALLBACK(testclass, propname)
Catch-2 tests for whether a NEW callback properly validates the input property properly.
Namespace for all libXWC tests.
#define XWCTEST_SETUP_INDI_ARB_PROP(varname, device, propname)
#define XWCTEST_SETUP_INDI_NEW_PROP(propname)