95TEST_CASE(
"Fourier temporal PSD workspace ownership is move-only",
"[ao::analysis::fourierTemporalPSD]" )
97 STATIC_REQUIRE_FALSE( std::is_copy_constructible_v<temporalPsdT> );
98 STATIC_REQUIRE_FALSE( std::is_copy_assignable_v<temporalPsdT> );
99 STATIC_REQUIRE( std::is_nothrow_move_constructible_v<temporalPsdT> );
100 STATIC_REQUIRE( std::is_nothrow_move_assignable_v<temporalPsdT> );
102 workspaceOwnershipTemporalPsdT source;
104 REQUIRE( source.ownsWorkspace() );
106 workspaceOwnershipTemporalPsdT destination( std::move( source ) );
107 REQUIRE_FALSE( source.ownsWorkspace() );
108 REQUIRE( destination.ownsWorkspace() );
109 REQUIRE( destination.m_aosys ==
nullptr );
118TEST_CASE(
"Fourier temporal PSD validates public calculation inputs",
"[ao::analysis::fourierTemporalPSD]" )
124 aoSystem.atm.
setSingleLayer( 0.16, 500e-9, 25.0, 0.0, 0.0, 10.0, 0.0 );
126 temporalPsdT temporalPsd;
127 temporalPsd.
m_aosys = &aoSystem;
130 std::vector<double> frequency{ 0.0, 1.0 };
131 std::vector<double> psd( frequency.size(), 7.0 );
132 const std::vector<double> unchanged = psd;
134 report.
record( GSL_SUCCESS, 0, 0.0, 0.0, 0.0, 1.0, 1.0 );
136 SECTION(
"null AO system" )
139 REQUIRE( temporalPsd.
singleLayerPSD( psd, frequency, 1.0, 0.0, 0, 1, frequency.back(), &report ) ==
143 SECTION(
"empty vectors" )
145 std::vector<double> empty;
149 SECTION(
"mismatched vector sizes" )
151 std::vector<double> undersizedPsd{ 7.0 };
152 REQUIRE( temporalPsd.
singleLayerPSD( undersizedPsd, frequency, 1.0, 0.0, 0, 1, frequency.back(), &report ) ==
154 REQUIRE( undersizedPsd == std::vector<double>{ 7.0 } );
157 SECTION(
"nonmonotone frequency grid" )
159 frequency[1] = frequency[0];
163 SECTION(
"negative frequency" )
169 SECTION(
"nonfinite frequency grid" )
171 frequency[1] = std::numeric_limits<double>::quiet_NaN();
175 SECTION(
"nonfinite mode" )
179 .singleLayerPSD( psd, frequency, std::numeric_limits<double>::infinity(), 0.0, 0, 1, 0, &report ) ==
183 SECTION(
"invalid parity" )
188 SECTION(
"negative cutoff" )
190 REQUIRE( temporalPsd.
singleLayerPSD( psd, frequency, 1.0, 0.0, 0, 1, -1.0, &report ) ==
194 SECTION(
"invalid layer index" )
199 SECTION(
"invalid tolerance" )
202 REQUIRE( temporalPsd.
singleLayerPSD( psd, frequency, 1.0, 0.0, 0, 1, 0, &report ) ==
206 SECTION(
"invalid relative tolerance" )
209 REQUIRE( temporalPsd.
singleLayerPSD( psd, frequency, 1.0, 0.0, 0, 1, 0, &report ) ==
213 SECTION(
"nonpositive aperture" )
216 REQUIRE( temporalPsd.
singleLayerPSD( psd, frequency, 1.0, 0.0, 0, 1, 0, &report ) ==
220 SECTION(
"zero wind" )
222 aoSystem.atm.
layer_v_wind( std::vector<double>{ 0.0 } );
223 REQUIRE( temporalPsd.
multiLayerPSD<
false>( psd, frequency, 1.0, 0.0, 1, 0, &report ) ==
227 SECTION(
"mismatched atmosphere vectors" )
229 aoSystem.atm.
layer_dir( std::vector<double>{} );
233 REQUIRE( psd == unchanged );
242TEST_CASE(
"Fourier temporal PSD reports workspace allocation failure",
"[ao::analysis::fourierTemporalPSD]" )
246 aoSystem.atm.
setSingleLayer( 0.16, 500e-9, 25.0, 0.0, 0.0, 10.0, 0.0 );
248 allocationFailureTemporalPsdT temporalPsd;
249 temporalPsd.m_aosys = &aoSystem;
252 std::vector<double> frequency{ 1.0 };
253 std::vector<double> psd{ 7.0 };
255 REQUIRE( temporalPsd.singleLayerPSD( psd, frequency, 1.0, 0.0, 0, 1, 0, &report ) ==
mx::error_t::allocerr );
256 REQUIRE( psd == std::vector<double>{ 7.0 } );
265TEST_CASE(
"Fourier temporal PSD validates grid-generation inputs",
"[ao::analysis::fourierTemporalPSD]" )
269 aoSystem.atm.
setSingleLayer( 0.16, 500e-9, 25.0, 0.0, 0.0, 10.0, 0.0 );
271 temporalPsdT temporalPsd;
272 temporalPsd.
m_aosys = &aoSystem;
275 const std::filesystem::path outputDirectory =
276 std::filesystem::temp_directory_path() /
277 (
"mxlib_fourierTemporalPSD_invalid_grid_" + std::to_string(
static_cast<long long>( getpid() ) ) );
278 std::error_code filesystemError;
279 std::filesystem::remove_all( outputDirectory, filesystemError );
280 REQUIRE_FALSE( filesystemError );
282 SECTION(
"empty output directory" )
287 SECTION(
"nonpositive spatial extent" )
292 SECTION(
"nonpositive frequency spacing" )
297 SECTION(
"nonfinite frequency spacing" )
300 temporalPsd.
makePSDGrid( outputDirectory.string(), 1, std::numeric_limits<double>::quiet_NaN(), 1.0 ) ==
304 SECTION(
"nonpositive maximum frequency" )
309 SECTION(
"negative exact-calculation cutoff" )
314 SECTION(
"unrepresentable sample count" )
316 REQUIRE( temporalPsd.
makePSDGrid( outputDirectory.string(), 1, std::numeric_limits<double>::min(), 1.0 ) ==
320 SECTION(
"invalid AO system" )
326 REQUIRE_FALSE( std::filesystem::exists( outputDirectory ) );
334TEST_CASE(
"Fourier temporal PSD reports grid-generation failures",
"[ao::analysis::fourierTemporalPSD]" )
338 aoSystem.atm.
setSingleLayer( 0.16, 500e-9, 25.0, 0.0, 0.0, 10.0, 0.0 );
340 const std::filesystem::path outputPath =
341 std::filesystem::temp_directory_path() /
342 (
"mxlib_fourierTemporalPSD_grid_failure_" + std::to_string(
static_cast<long long>( getpid() ) ) );
343 std::error_code filesystemError;
344 std::filesystem::remove_all( outputPath, filesystemError );
345 REQUIRE_FALSE( filesystemError );
347 SECTION(
"mode calculation failure" )
349 allocationFailureTemporalPsdT temporalPsd;
350 temporalPsd.m_aosys = &aoSystem;
354 REQUIRE( std::filesystem::exists( outputPath /
"params.txt" ) );
355 REQUIRE( std::filesystem::exists( outputPath /
"psds" /
"freq.binv" ) );
358 SECTION(
"output path is a regular file" )
360 std::ofstream outputFile( outputPath );
361 REQUIRE( outputFile.is_open() );
364 temporalPsdT temporalPsd;
365 temporalPsd.
m_aosys = &aoSystem;
370 std::filesystem::remove_all( outputPath, filesystemError );
371 REQUIRE_FALSE( filesystemError );
382TEST_CASE(
"Fourier temporal PSD enforces atmosphere validity at calculation",
"[ao::analysis::fourierTemporalPSD]" )
387 temporalPsdT temporalPsd;
388 temporalPsd.
m_aosys = &aoSystem;
391 std::vector<double> frequency{ 1.0 };
392 std::vector<double> psd{ 7.0 };
394 SECTION(
"default atmosphere" )
397 REQUIRE( temporalPsd.
singleLayerPSD( psd, frequency, 1.0, 0.0, 0, 1, frequency.back() ) ==
399 REQUIRE( psd == std::vector<double>{ 7.0 } );
402 SECTION(
"Guyon 2005 preset with infinite outer scale" )
405 REQUIRE( aoSystem.atm.
L_0( 0 ) == 0.0 );
407 REQUIRE( temporalPsd.
singleLayerPSD( psd, frequency, 1.0, 0.0, 0, 1, frequency.back() ) ==
411 SECTION(
"LCO preset" )
415 REQUIRE( temporalPsd.
singleLayerPSD( psd, frequency, 1.0, 0.0, 0, 1, frequency.back() ) ==
419 SECTION(
"single-layer preset" )
421 aoSystem.atm.
setSingleLayer( 0.2, 0.5e-6, 25.0, 0.0, 0.0, 10.0, 0.0 );
423 REQUIRE( temporalPsd.
singleLayerPSD( psd, frequency, 1.0, 0.0, 0, 1, frequency.back() ) ==
433TEST_CASE(
"Fourier temporal PSD handles short exact tails",
"[ao::analysis::fourierTemporalPSD]" )
437 aoSystem.atm.
setSingleLayer( 0.16, 500e-9, 25.0, 0.0, 0.0, 10.0, 0.0 );
439 temporalPsdT temporalPsd;
440 temporalPsd.
m_aosys = &aoSystem;
443 constexpr std::size_t frequencyCount = 55;
444 std::vector<double> frequency( frequencyCount );
445 for( std::size_t index = 0; index < frequency.size(); ++index )
447 frequency[index] =
static_cast<double>( index + 1 );
450 for(
const std::size_t exactCount : { 0U, 1U, 49U, 50U } )
452 DYNAMIC_SECTION( exactCount <<
" exact bins" )
454 std::vector<double> psd( frequency.size(), 0.0 );
455 const double maximumExactFrequency = exactCount == 0 ? 0.5 : frequency[exactCount - 1];
459 temporalPsd.
singleLayerPSD( psd, frequency, 1.0, 0.0, 0, 1, maximumExactFrequency, &report );
461 if( exactCount == 0 )
471 const std::size_t averageCount = std::min<std::size_t>( exactCount, 50 );
472 const double exponent = aoSystem.atm.
alpha( 0 ) + 2.0;
473 double expected = 0.0;
474 for( std::size_t offset = averageCount; offset > 0; --offset )
476 const std::size_t index = exactCount - offset;
477 expected += psd[index] * std::pow( frequency[index] / frequency[exactCount], exponent );
479 expected /=
static_cast<double>( averageCount );
481 REQUIRE( psd[exactCount] == Approx( expected ).epsilon( 1e-12 ) );
482 REQUIRE( std::isfinite( psd[exactCount] ) );
483 REQUIRE( psd[exactCount] >= 0.0 );
485 const double expectedLast =
486 psd[exactCount] * std::pow( frequency[exactCount] / frequency.back(), exponent );
487 REQUIRE( psd.back() == Approx( expectedLast ).epsilon( 1e-12 ) );
498TEST_CASE(
"Fourier temporal PSD aggregates quadrature diagnostics",
"[ao::analysis::fourierTemporalPSD]" )
501 report.
record( GSL_SUCCESS, 0, 1.0, 2.0, 0.1, 0.1, 0.01 );
502 report.
record( GSL_EROUND, 0, 2.0, 2.0, 0.4, 0.1, 0.01 );
503 report.
record( GSL_EROUND, 1, 3.0, 4.0, 1.0, 0.1, 0.01 );
506 other.
record( GSL_EDIVERGE, 2, 4.0, 1.0, 0.5, 0.1, 0.01 );
507 report.
merge( other );
512 REQUIRE( report.
gslStatus.at( GSL_EROUND ).count == 2 );
513 REQUIRE( report.
gslStatus.at( GSL_EROUND ).countByLayer.at( 0 ) == 1 );
514 REQUIRE( report.
gslStatus.at( GSL_EROUND ).countByLayer.at( 1 ) == 1 );
515 REQUIRE( report.
gslStatus.at( GSL_EROUND ).maximumAbsoluteError == Approx( 1.0 ) );
516 REQUIRE( report.
gslStatus.at( GSL_EROUND ).maximumToleranceRatio == Approx( 10.0 ) );
517 REQUIRE( report.
gslStatus.at( GSL_EROUND ).worstLayer == 1 );
518 REQUIRE( report.
gslStatus.at( GSL_EROUND ).worstFrequency == Approx( 3.0 ) );
519 REQUIRE( report.
gslStatus.at( GSL_EDIVERGE ).count == 1 );
521 std::ostringstream summary;
522 report.
write( summary );
523 REQUIRE( summary.str().find(
"1/4 converged" ) != std::string::npos );
524 REQUIRE( summary.str().find( gsl_strerror( GSL_EROUND ) ) != std::string::npos );
525 REQUIRE( summary.str().find( gsl_strerror( GSL_EDIVERGE ) ) != std::string::npos );
526 REQUIRE( summary.str().find(
"layers {0: 1, 1: 1}" ) != std::string::npos );
534TEST_CASE(
"Fourier temporal PSD applies quadrature policy",
"[ao::analysis::fourierTemporalPSD]" )
537 using mx::AO::analysis::fourierTemporalPSD_detail::applyPolicy;
539 REQUIRE( applyPolicy( GSL_SUCCESS, fourierTemporalPSDPolicy::permissive ) ==
mx::error_t::noerror );
540 for(
const int status : { GSL_EMAXITER, GSL_EROUND, GSL_ESING, GSL_EDIVERGE } )
548 REQUIRE( applyPolicy( GSL_EFAILED, fourierTemporalPSDPolicy::permissive ) ==
mx::error_t::liberr );