mxlib
c++ tools for analyzing astronomical data and other tasks by Jared R. Males. [git repo]
Loading...
Searching...
No Matches
clGainOpt_test.cpp
Go to the documentation of this file.
1/** \file clGainOpt_test.cpp
2 * \brief Tests of closed-loop gain optimization transfer functions.
3 */
4
5#include "../../../catch2/catch.hpp"
6
7#define MX_NO_ERROR_REPORTS
8
10
11#include <cmath>
12#include <complex>
13#include <numbers>
14#include <vector>
15
16namespace
17{
18
20
21/// Require two complex values to agree within floating-point precision.
22void requireComplexEqual( const std::complex<double> &actual, /**< [in] value produced by the retimed optimizer */
23 const std::complex<double> &expected /**< [in] value produced by the fresh optimizer */ )
24{
25 REQUIRE( actual.real() == Approx( expected.real() ).epsilon( 1e-12 ).margin( 1e-14 ) );
26 REQUIRE( actual.imag() == Approx( expected.imag() ).epsilon( 1e-12 ).margin( 1e-14 ) );
27}
28
29} // namespace
30
31/// Verify that changing the sampling interval invalidates all sampling-interval-dependent cached values.
32/** Exercises mx::AO::analysis::clGainOpt::Ti and the public transfer-function calculations after the trigonometric
33 * cache has already been populated.
34 */
35/**
36 * \ingroup clGainOpt_unit_tests
37 */
38TEST_CASE( "Gain optimizer recomputes transfer functions after changing Ti", "[ao::analysis::clGainOpt]" )
39{
40 constexpr double initialTi = 0.001;
41 constexpr double updatedTi = 0.0017;
42 constexpr double delay = 0.0013;
43 constexpr double gain = 0.42;
44
45 const std::vector<double> frequency{ 25.0, 50.0, 75.0, 100.0 };
46 const std::vector<double> fir{ 0.75, 0.20, -0.05 };
47 const std::vector<double> iir{ 0.45, -0.08, 0.025 };
48
49 optimizerT retimed( initialTi, delay );
50 retimed.f( frequency );
51 retimed.b( fir );
52 retimed.a( iir );
53 retimed.remember( 0.97 );
54
55 for( std::size_t index = 0; index < frequency.size(); ++index )
56 {
57 retimed.olXfer( index );
58 }
59
60 retimed.Ti( updatedTi );
61
62 optimizerT fresh( updatedTi, delay );
63 fresh.f( frequency );
64 fresh.b( fir );
65 fresh.a( iir );
66 fresh.remember( 0.97 );
67
68 for( std::size_t index = 0; index < frequency.size(); ++index )
69 {
70 CAPTURE( index, frequency[index] );
71
72 optimizerT::complexT retimedDm;
73 optimizerT::complexT retimedDelay;
74 optimizerT::complexT retimedController;
75 const optimizerT::complexT retimedOpenLoop =
76 retimed.olXfer( index, retimedDm, retimedDelay, retimedController );
77
78 optimizerT::complexT freshDm;
79 optimizerT::complexT freshDelay;
80 optimizerT::complexT freshController;
81 const optimizerT::complexT freshOpenLoop = fresh.olXfer( index, freshDm, freshDelay, freshController );
82
83 requireComplexEqual( retimedDm, freshDm );
84 requireComplexEqual( retimedDelay, freshDelay );
85 requireComplexEqual( retimedController, freshController );
86 requireComplexEqual( retimedOpenLoop, freshOpenLoop );
87 requireComplexEqual( retimed.clETF( index, gain ), fresh.clETF( index, gain ) );
88 REQUIRE( retimed.clETFPhase( index, gain ) ==
89 Approx( fresh.clETFPhase( index, gain ) ).epsilon( 1e-12 ).margin( 1e-14 ) );
90 REQUIRE( retimed.clETF2( index, gain ) ==
91 Approx( fresh.clETF2( index, gain ) ).epsilon( 1e-12 ).margin( 1e-14 ) );
92 requireComplexEqual( retimed.clNTF( index, gain ), fresh.clNTF( index, gain ) );
93 REQUIRE( retimed.clNTF2( index, gain ) ==
94 Approx( fresh.clNTF2( index, gain ) ).epsilon( 1e-12 ).margin( 1e-14 ) );
95
96 double retimedEtf = 0;
97 double retimedNtf = 0;
98 retimed.clTF2( retimedEtf, retimedNtf, index, gain );
99
100 double freshEtf = 0;
101 double freshNtf = 0;
102 fresh.clTF2( freshEtf, freshNtf, index, gain );
103
104 REQUIRE( retimedEtf == Approx( freshEtf ).epsilon( 1e-12 ).margin( 1e-14 ) );
105 REQUIRE( retimedNtf == Approx( freshNtf ).epsilon( 1e-12 ).margin( 1e-14 ) );
106 }
107
108 const std::vector<double> disturbancePsd{ 4.0, 2.0, 0.7, 0.2 };
109 const std::vector<double> noisePsd{ 0.01, 0.01, 0.01, 0.01 };
110 REQUIRE( retimed.clVariance( disturbancePsd, noisePsd, gain ) ==
111 Approx( fresh.clVariance( disturbancePsd, noisePsd, gain ) ).epsilon( 1e-12 ).margin( 1e-14 ) );
112
113 double retimedOptimalVariance = 0;
114 double retimedOptimalGain = 0;
115 REQUIRE(
116 retimed.optGainOpenLoop( retimedOptimalGain, retimedOptimalVariance, disturbancePsd, noisePsd, 0.5, false ) ==
118
119 double freshOptimalVariance = 0;
120 double freshOptimalGain = 0;
121 REQUIRE( fresh.optGainOpenLoop( freshOptimalGain, freshOptimalVariance, disturbancePsd, noisePsd, 0.5, false ) ==
123
124 REQUIRE( retimedOptimalGain == Approx( freshOptimalGain ).epsilon( 1e-12 ).margin( 1e-14 ) );
125 REQUIRE( retimedOptimalVariance == Approx( freshOptimalVariance ).epsilon( 1e-12 ).margin( 1e-14 ) );
126}
127
128/// Verify that maximum-stable-gain calculation interpolates a bracketed pure-integrator Nyquist crossing.
129/** Exercises mx::AO::analysis::clGainOpt::maxStableGain and its crossing diagnostics. */
130/**
131 * \ingroup clGainOpt_unit_tests
132 */
133TEST_CASE( "Gain optimizer interpolates a pure-integrator stability crossing", "[ao::analysis::clGainOpt]" )
134{
135 optimizerT optimizer( 0.001, 0.0015 );
136 const std::vector<double> frequency{ 100.0, 124.0, 126.0, 150.0 };
137 optimizer.f( frequency );
138
139 const double crossingPhase = std::numbers::pi_v<double> / 4.0;
140 const double expectedGain = crossingPhase * crossingPhase / ( 2.0 * std::sin( crossingPhase / 2.0 ) );
141 const double lowerSampleGain = -1.0 / optimizer.olXfer( 1 ).real();
142
143 double maximumGain = 0;
144 optimizerT::maxStableGainReport report;
145 REQUIRE( optimizer.maxStableGain( maximumGain, &report ) == mx::error_t::noerror );
146
147 REQUIRE( report.status == optimizerT::maxStableGainStatus::crossingFound );
148 REQUIRE( report.lowerIndex == 1 );
149 REQUIRE( report.upperIndex == 2 );
150 REQUIRE( report.lowerFrequency == 124.0 );
151 REQUIRE( report.upperFrequency == 126.0 );
152 REQUIRE( report.crossingFrequency == Approx( 125.0 ).margin( 0.02 ) );
153 REQUIRE( maximumGain == report.gain );
154 REQUIRE( std::abs( maximumGain - expectedGain ) < std::abs( lowerSampleGain - expectedGain ) );
155}
156
157/// Verify that maximum-stable-gain calculation reports invalid grids and missing crossings.
158/** Exercises mx::AO::analysis::clGainOpt::maxStableGain failure statuses without sentinel gain values. */
159/**
160 * \ingroup clGainOpt_unit_tests
161 */
162TEST_CASE( "Gain optimizer reports missing stability crossings", "[ao::analysis::clGainOpt]" )
163{
164 optimizerT optimizer( 0.001, 0.0015 );
165 double maximumGain = 0;
166 optimizerT::maxStableGainReport report;
167
168 optimizer.f( std::vector<double>{ 1.0 } );
169 REQUIRE( optimizer.maxStableGain( maximumGain, &report ) == mx::error_t::sizeerr );
170 REQUIRE( report.status == optimizerT::maxStableGainStatus::invalidInput );
171 REQUIRE( mx::math::isNan( maximumGain ) );
172
173 optimizer.f( std::vector<double>{ 1.0, 2.0, 3.0 } );
174 REQUIRE( optimizer.maxStableGain( maximumGain, &report ) == mx::error_t::notfound );
175 REQUIRE( report.status == optimizerT::maxStableGainStatus::noCrossing );
176 REQUIRE( mx::math::isNan( maximumGain ) );
177}
178
179/// Verify that optimum-gain calculation enforces its interval and reports termination state.
180/** Exercises both mx::AO::analysis::clGainOpt::optGainOpenLoop overloads for small intervals, invalid intervals,
181 * stability failure, and forced iteration exhaustion.
182 */
183/**
184 * \ingroup clGainOpt_unit_tests
185 */
186TEST_CASE( "Gain optimizer reports minimizer termination", "[ao::analysis::clGainOpt]" )
187{
188 optimizerT optimizer( 0.001, 0.0015 );
189 std::vector<double> frequency;
190 std::vector<double> disturbance;
191 std::vector<double> noise;
192 for( int index = 1; index <= 500; ++index )
193 {
194 const double value = static_cast<double>( index );
195 frequency.push_back( value );
196 disturbance.push_back( 1.0 / ( 1.0 + value * value ) );
197 noise.push_back( 1e-6 );
198 }
199 optimizer.f( frequency );
200
201 double optimalGain = 0;
202 double variance = 0;
203 optimizerT::optGainReport report;
204 constexpr double smallMaximumGain = 1e-3;
205 REQUIRE( optimizer.optGainOpenLoop( optimalGain, variance, disturbance, noise, smallMaximumGain, true, &report ) ==
207 REQUIRE( ( report.status == optimizerT::optGainStatus::converged ||
208 report.status == optimizerT::optGainStatus::boundaryLimited ) );
209 REQUIRE( report.minimumEvaluatedGain >= optimizer.m_minFindMin );
210 REQUIRE( report.maximumEvaluatedGain <= optimizer.m_minFindMaxFact * smallMaximumGain );
211 REQUIRE( mx::math::isFinite( optimalGain ) );
212 REQUIRE( mx::math::isFinite( variance ) );
213
214 REQUIRE( optimizer.optGainOpenLoop( optimalGain, variance, disturbance, noise, 1e-10, false, &report ) ==
216 REQUIRE( report.status == optimizerT::optGainStatus::invalidInput );
217 REQUIRE( mx::math::isNan( optimalGain ) );
218 REQUIRE( mx::math::isNan( variance ) );
219
220 optimizer.f( std::vector<double>{ 1.0, 2.0, 3.0 } );
221 disturbance.resize( 3 );
222 noise.resize( 3 );
223 REQUIRE( optimizer.optGainOpenLoop( optimalGain, variance, disturbance, noise, false, &report ) ==
225 REQUIRE( report.status == optimizerT::optGainStatus::stabilityFailure );
226 REQUIRE( report.stability.status == optimizerT::maxStableGainStatus::noCrossing );
227
228 optimizer.f( frequency );
229 disturbance.resize( frequency.size(), 1e-3 );
230 noise.resize( frequency.size(), 1e-6 );
231 optimizer.m_minFindMaxIter = 1;
232 REQUIRE( optimizer.optGainOpenLoop( optimalGain, variance, disturbance, noise, 0.5, false, &report ) ==
234 REQUIRE( report.status == optimizerT::optGainStatus::iterationLimit );
235 REQUIRE( report.iterations == 1 );
236}
Provides a class to manage closed loop gain optimization.
TEST_CASE("Gain optimizer recomputes transfer functions after changing Ti", "[ao::analysis::clGainOpt]")
Verify that changing the sampling interval invalidates all sampling-interval-dependent cached values.
@ noerror
No error has occurred.
Definition error_t.hpp:27
@ sizeerr
A size was invalid or calculated incorrectly.
Definition error_t.hpp:35
@ timeout
A timeout occurred.
Definition error_t.hpp:49
@ invalidconfig
A config setting was invalid.
Definition error_t.hpp:30
@ notfound
An item was not found.
Definition error_t.hpp:34
bool isNan(realT value)
Test whether a floating-point value is NaN, including under finite-math-only optimization.
bool isFinite(realT value)
Test whether a floating-point value is finite, including under finite-math-only optimization.
A class to manage optimizing closed-loop gains.
Definition clGainOpt.hpp:69