mxlib
c++ tools for analyzing astronomical data and other tasks by Jared R. Males. [git repo]
Loading...
Searching...
No Matches
eigenCube_test.cpp
Go to the documentation of this file.
1/** \file eigenCube_test.cpp
2 * \brief Tests of masked image combination in eigenCube.
3 * \author OpenAI Codex
4 */
5
6#include "../../catch2/catch.hpp"
7
8#include <limits>
9#include <utility>
10#include <vector>
11
13
14/** \cond
15 * Explicit instantiation compile-checks the representative double-precision cube and emits its header-defined
16 * methods for coverage accounting in this test translation unit.
17 */
19
20/// Emit the exact float specializations called by HCI observation processing for LCOV accounting.
25/** \endcond */
26
27namespace unitTest
28{
29namespace improcTest
30{
31namespace eigenCubeTest
32{
33
34/// Verify masked mean combinations accept an inclusive good-pixel fraction and use the cube scalar sentinel.
35/** Exercises mx::improc::eigenCube::mean for masked and weighted-masked combinations. */
36/**
37 * \ingroup eigenCube_unit_tests
38 */
39TEST_CASE( "Masked eigenCube means use an inclusive good-pixel threshold", "[improc::eigenCube]" )
40{
41 mx::improc::eigenCube<double> cube( 1, 1, 4 );
42 cube.image( 0 )( 0, 0 ) = 2.0;
43 cube.image( 1 )( 0, 0 ) = 4.0;
44 cube.image( 2 )( 0, 0 ) = 8.0;
45 cube.image( 3 )( 0, 0 ) = 16.0;
46
47 mx::improc::eigenCube<int> mask( 1, 1, 4 );
48 mask.image( 0 )( 0, 0 ) = 1;
49 mask.image( 1 )( 0, 0 ) = 1;
50 mask.image( 2 )( 0, 0 ) = 0;
51 mask.image( 3 )( 0, 0 ) = 0;
52
54
55 cube.mean( result, mask, 0.49 );
56 REQUIRE( result( 0, 0 ) == 3.0 );
57
58 cube.mean( result, mask, 0.5 );
59 REQUIRE( result( 0, 0 ) == 3.0 );
60
61 cube.mean( result, mask, 0.51 );
62 REQUIRE( mx::improc::isInvalidPixel( result( 0, 0 ) ) );
63
64 std::vector<double> weights{ 1.0, 3.0, 5.0, 7.0 };
65 cube.mean( result, weights, mask, 0.5 );
66 REQUIRE( result( 0, 0 ) == 3.5 );
67
68 mask.setZero();
69 cube.mean( result, mask, 0.0 );
70 REQUIRE( mx::improc::isInvalidPixel( result( 0, 0 ) ) );
71
72 mask.image( 0 )( 0, 0 ) = 1;
73 cube.mean( result, mask, 0.0 );
74 REQUIRE( result( 0, 0 ) == 2.0 );
75
76 for( int plane = 0; plane < mask.planes(); ++plane )
77 {
78 mask.image( plane )( 0, 0 ) = 1;
79 }
80
81 cube.mean( result, mask, 1.0 );
82 REQUIRE( result( 0, 0 ) == 7.5 );
83}
84
85/** \brief Verifies unmasked and weighted image combinations used when HCI masking is disabled.
86 *
87 * \ingroup eigenCube_unit_tests
88 */
89TEST_CASE( "Unmasked eigenCube combinations use every plane", "[improc::eigenCube]" )
90{
91 mx::improc::eigenCube<double> cube( 1, 1, 4 );
92 cube.image( 0 )( 0, 0 ) = 1.0;
93 cube.image( 1 )( 0, 0 ) = 2.0;
94 cube.image( 2 )( 0, 0 ) = 3.0;
95 cube.image( 3 )( 0, 0 ) = 100.0;
96
98 std::vector<double> weights{ 1.0, 1.0, 1.0, 3.0 };
99
100 cube.mean( result );
101 REQUIRE( result( 0, 0 ) == 26.5 );
102
103 cube.mean( result, weights );
104 REQUIRE( result( 0, 0 ) == 51.0 );
105
106 cube.median( result );
107 REQUIRE( result( 0, 0 ) == 2.5 );
108
109 cube.sigmaMean( result, 100.0 );
110 REQUIRE( result( 0, 0 ) == 26.5 );
111
112 cube.sigmaMean( result, weights, 100.0 );
113 REQUIRE( result( 0, 0 ) == 51.0 );
114}
115
116/** \brief Verifies float cube zeroing and image combinations used by HCI observation processing.
117 *
118 * Exercises mx::improc::eigenCube<float>::setZero, mean, and both median overloads, including all float
119 * mx::improc::imageMedian workspace and mask paths.
120 *
121 * \ingroup eigenCube_unit_tests
122 */
123TEST_CASE( "Float eigenCube combinations clear and combine HCI images", "[improc::eigenCube]" )
124{
125 mx::improc::eigenCube<float> cube( 1, 1, 4 );
126 cube.setZero();
127 for( int plane = 0; plane < cube.planes(); ++plane )
128 {
129 REQUIRE( cube.image( plane )( 0, 0 ) == 0.0F );
130 }
131
132 cube.image( 0 )( 0, 0 ) = 1.0F;
133 cube.image( 1 )( 0, 0 ) = 2.0F;
134 cube.image( 2 )( 0, 0 ) = 7.0F;
135 cube.image( 3 )( 0, 0 ) = 10.0F;
136
138 cube.mean( result );
139 REQUIRE( result( 0, 0 ) == 5.0F );
140
141 cube.median( result );
142 REQUIRE( result( 0, 0 ) == 4.5F );
143
144 mx::improc::eigenCube<int> mask( 1, 1, 4 );
145 mask.image( 0 )( 0, 0 ) = 1;
146 mask.image( 1 )( 0, 0 ) = 0;
147 mask.image( 2 )( 0, 0 ) = 1;
148 mask.image( 3 )( 0, 0 ) = 0;
149 cube.median( result, mask, 0.5 );
150 REQUIRE( result( 0, 0 ) == 4.0F );
151
152 auto values = cube.pixel( 0, 0 );
153 mx::improc::eigenImage<float> valueMask( 4, 1 );
154 valueMask << 1.0F, 0.0F, 1.0F, 0.0F;
155 std::vector<float> work;
156 REQUIRE( mx::improc::imageMedian( values, &valueMask, &work ) == 4.0F );
157 REQUIRE( mx::improc::imageMedian( values ) == 4.5F );
158}
159
160/// Verify masked median combination honors the mask and inclusive good-pixel fraction.
161/** Exercises mx::improc::eigenCube::median, including its even-sample median behavior. */
162/**
163 * \ingroup eigenCube_unit_tests
164 */
165TEST_CASE( "Masked eigenCube median honors its mask and threshold", "[improc::eigenCube]" )
166{
167 mx::improc::eigenCube<double> cube( 1, 1, 4 );
168 cube.image( 0 )( 0, 0 ) = 1.0;
169 cube.image( 1 )( 0, 0 ) = 100.0;
170 cube.image( 2 )( 0, 0 ) = 5.0;
171 cube.image( 3 )( 0, 0 ) = 9.0;
172
173 mx::improc::eigenCube<int> mask( 1, 1, 4 );
174 mask.image( 0 )( 0, 0 ) = 1;
175 mask.image( 1 )( 0, 0 ) = 0;
176 mask.image( 2 )( 0, 0 ) = 1;
177 mask.image( 3 )( 0, 0 ) = 0;
178
180
181 cube.median( result, mask, 0.5 );
182 REQUIRE( result( 0, 0 ) == 3.0 );
183
184 cube.median( result, mask, 0.5001 );
185 REQUIRE( mx::improc::isInvalidPixel( result( 0, 0 ) ) );
186
187 mask.setZero();
188 cube.median( result, mask, 0.0 );
189 REQUIRE( mx::improc::isInvalidPixel( result( 0, 0 ) ) );
190
191 for( int plane = 0; plane < mask.planes(); ++plane )
192 {
193 mask.image( plane )( 0, 0 ) = 1;
194 }
195
196 cube.median( result, mask, 1.0 );
197 REQUIRE( result( 0, 0 ) == 7.0 );
198}
199
200/// Verify masked sigma means apply the inclusive threshold with and without weights.
201/** Exercises mx::improc::eigenCube::sigmaMean for masked and weighted-masked combinations. */
202/**
203 * \ingroup eigenCube_unit_tests
204 */
205TEST_CASE( "Masked eigenCube sigma means use an inclusive good-pixel threshold", "[improc::eigenCube]" )
206{
207 mx::improc::eigenCube<double> cube( 1, 1, 4 );
208 cube.image( 0 )( 0, 0 ) = 2.0;
209 cube.image( 1 )( 0, 0 ) = 4.0;
210 cube.image( 2 )( 0, 0 ) = 8.0;
211 cube.image( 3 )( 0, 0 ) = 16.0;
212
213 mx::improc::eigenCube<int> mask( 1, 1, 4 );
214 mask.image( 0 )( 0, 0 ) = 1;
215 mask.image( 1 )( 0, 0 ) = 1;
216 mask.image( 2 )( 0, 0 ) = 0;
217 mask.image( 3 )( 0, 0 ) = 0;
218
220 std::vector<double> weights{ 1.0, 3.0, 5.0, 7.0 };
221
222 cube.sigmaMean( result, mask, 100.0, 0.5 );
223 REQUIRE( result( 0, 0 ) == 3.0 );
224
225 cube.sigmaMean( result, weights, mask, 100.0, 0.5 );
226 REQUIRE( result( 0, 0 ) == 3.5 );
227
228 cube.sigmaMean( result, mask, 100.0, 0.51 );
229 REQUIRE( mx::improc::isInvalidPixel( result( 0, 0 ) ) );
230}
231
232/// Verify masked combinations reject invalid masks, weights, and good-pixel thresholds.
233/** Exercises argument validation in mx::improc::eigenCube::mean, median, and sigmaMean. */
234/**
235 * \ingroup eigenCube_unit_tests
236 */
237TEST_CASE( "eigenCube combinations validate masks weights and thresholds", "[improc::eigenCube]" )
238{
239 mx::improc::eigenCube<double> cube( 1, 1, 4 );
240 cube.setZero();
241
242 mx::improc::eigenCube<int> mask( 1, 1, 4 );
243 mask.setZero();
244
245 mx::improc::eigenCube<int> wrongMask( 1, 2, 4 );
246 wrongMask.setZero();
247
249 std::vector<double> weights{ 1.0, 1.0, 1.0, 1.0 };
250 std::vector<double> wrongWeights{ 1.0, 1.0, 1.0 };
251
252 try
253 {
254 cube.mean( result, wrongMask );
255 FAIL( "mismatched mask dimensions were accepted" );
256 }
257 catch( const mx::exception<> &error )
258 {
259 REQUIRE( error.code() == mx::error_t::sizeerr );
260 }
261
262 try
263 {
264 cube.mean( result, wrongWeights );
265 FAIL( "mismatched weights were accepted" );
266 }
267 catch( const mx::exception<> &error )
268 {
269 REQUIRE( error.code() == mx::error_t::sizeerr );
270 }
271
272 try
273 {
274 cube.sigmaMean( result, wrongWeights, 3.0 );
275 FAIL( "mismatched sigma-mean weights were accepted" );
276 }
277 catch( const mx::exception<> &error )
278 {
279 REQUIRE( error.code() == mx::error_t::sizeerr );
280 }
281
282 try
283 {
284 cube.mean( result, mask, -0.01 );
285 FAIL( "a negative threshold was accepted" );
286 }
287 catch( const mx::exception<> &error )
288 {
289 REQUIRE( error.code() == mx::error_t::invalidarg );
290 }
291
292 try
293 {
294 cube.median( result, mask, 1.01 );
295 FAIL( "a threshold above one was accepted" );
296 }
297 catch( const mx::exception<> &error )
298 {
299 REQUIRE( error.code() == mx::error_t::invalidarg );
300 }
301
302 try
303 {
304 cube.sigmaMean( result, mask, 3.0, std::numeric_limits<double>::quiet_NaN() );
305 FAIL( "a NaN threshold was accepted" );
306 }
307 catch( const mx::exception<> &error )
308 {
309 REQUIRE( error.code() == mx::error_t::invalidarg );
310 }
311
312 try
313 {
314 cube.sigmaMean( result, weights, mask, 3.0, std::numeric_limits<double>::infinity() );
315 FAIL( "an infinite threshold was accepted" );
316 }
317 catch( const mx::exception<> &error )
318 {
319 REQUIRE( error.code() == mx::error_t::invalidarg );
320 }
321}
322
323/// Verify eigenCube copies own independent storage and moves transfer ownership safely.
324/** Exercises mx::improc::eigenCube copy construction, assignment, move construction, move assignment, and shallowCopy.
325 */
326/**
327 * \ingroup eigenCube_unit_tests
328 */
329TEST_CASE( "eigenCube copy and move operations preserve ownership", "[improc::eigenCube]" )
330{
331 mx::improc::eigenCube<double> source( 1, 2, 2 );
332 source.image( 0 ) << 1.0, 2.0;
333 source.image( 1 ) << 3.0, 4.0;
334
335 mx::improc::eigenCube<double> copied( source );
336 REQUIRE( copied.data() != source.data() );
337 REQUIRE( copied.image( 1 )( 0, 1 ) == 4.0 );
338 copied.image( 0 )( 0, 0 ) = 10.0;
339 REQUIRE( source.image( 0 )( 0, 0 ) == 1.0 );
340
342 assigned = source;
343 REQUIRE( assigned.data() != source.data() );
344 REQUIRE( assigned.image( 1 )( 0, 0 ) == 3.0 );
345
346 mx::improc::eigenCube<double> moved( std::move( copied ) );
347 REQUIRE( copied.data() == nullptr );
348 REQUIRE( copied.rows() == 0 );
349 REQUIRE( moved.image( 0 )( 0, 0 ) == 10.0 );
350
352 moveAssigned = std::move( assigned );
353 REQUIRE( assigned.data() == nullptr );
354 REQUIRE( assigned.planes() == 0 );
355 REQUIRE( moveAssigned.image( 1 )( 0, 1 ) == 4.0 );
356
358 transferred.shallowCopy( source, true );
359 REQUIRE( source.data() == nullptr );
360 REQUIRE( source.cols() == 0 );
361 REQUIRE( transferred.image( 0 )( 0, 1 ) == 2.0 );
362
363 transferred.shallowCopy( transferred, true );
364 REQUIRE( transferred.image( 1 )( 0, 0 ) == 3.0 );
365}
366
367/** \brief Verifies non-owning storage, self-assignment, resized views, and covariance operations.
368 *
369 * Exercises mx::improc::eigenCube's external-storage constructor, non-owning shallowCopy, two-dimensional resize,
370 * cube, asVectors, Covar, and self copy/move assignment paths.
371 *
372 * \ingroup eigenCube_unit_tests
373 */
374TEST_CASE( "eigenCube storage views and covariance preserve layout", "[improc::eigenCube]" )
375{
376 double externalData[4]{ 1.0, 2.0, 3.0, 4.0 };
377 mx::improc::eigenCube<double> externalCube( externalData, 1, 2, 2 );
378 REQUIRE( externalCube.data() == externalData );
379 REQUIRE( externalCube.image( 1 )( 0, 1 ) == 4.0 );
380
382 borrowed.shallowCopy( externalCube, false );
383 REQUIRE( borrowed.data() == externalData );
384 REQUIRE( externalCube.data() == externalData );
385
386 externalCube = externalCube;
387 externalCube = std::move( externalCube );
388 REQUIRE( externalCube.data() == externalData );
389 REQUIRE( externalCube.rows() == 1 );
390 REQUIRE( externalCube.cols() == 2 );
391 REQUIRE( externalCube.planes() == 2 );
392
393 REQUIRE( externalCube.cube().rows() == 2 );
394 REQUIRE( externalCube.cube().cols() == 2 );
395 REQUIRE( externalCube.asVectors()( 1, 1 ) == 4.0 );
396
397 Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic> covariance;
398 externalCube.Covar( covariance );
399 REQUIRE( covariance.rows() == 2 );
400 REQUIRE( covariance.cols() == 2 );
401 REQUIRE( covariance( 0, 0 ) == 5.0 );
402 REQUIRE( covariance( 0, 1 ) == 11.0 );
403 REQUIRE( covariance( 1, 0 ) == 11.0 );
404 REQUIRE( covariance( 1, 1 ) == 25.0 );
405
406 borrowed.clear();
407 REQUIRE( borrowed.data() == nullptr );
408 REQUIRE( externalData[3] == 4.0 );
409
411 resized.resize( 2, 3 );
412 REQUIRE( resized.rows() == 2 );
413 REQUIRE( resized.cols() == 3 );
414 REQUIRE( resized.planes() == 1 );
415}
416
417} // namespace eigenCubeTest
418} // namespace improcTest
419} // namespace unitTest
Augments an exception with the source file and line.
Definition exception.hpp:42
An image cube with an Eigen-like API.
Definition eigenCube.hpp:33
void Covar(Eigen::Matrix< dataT, Eigen::Dynamic, Eigen::Dynamic > &cv)
Calculate the covariance matrix of the images in the cube.
void sigmaMean(eigenT &mim, Scalar sigma)
Calculate the sigma clipped mean image of the cube.
void clear()
De-allocate and set all sizes to 0.
Eigen::Map< Eigen::Array< dataT, Eigen::Dynamic, Eigen::Dynamic >, Eigen::Unaligned, Eigen::Stride< Eigen::Dynamic, Eigen::Dynamic > > pixel(Index i, Index j)
Returns an Eigen::Eigen::Map-ed vector of the pixels at the given coordinate.
void median(eigenT &mim)
Calculate the median image of the cube.
Eigen::Map< Eigen::Array< dataT, Eigen::Dynamic, Eigen::Dynamic > > cube()
Returns a 2D Eigen::Eigen::Map pointed at the entire cube.
void mean(eigenT &mim)
Calculate the mean image of the cube.
Eigen::Map< Eigen::Array< dataT, Eigen::Dynamic, Eigen::Dynamic > > asVectors()
Return an Eigen::Eigen::Map of the cube where each image is a vector.
Eigen::Map< Eigen::Array< dataT, Eigen::Dynamic, Eigen::Dynamic > > image(Index n)
Returns a 2D Eigen::Eigen::Map pointed at the specified image.
An image cube with an Eigen API.
TEST_CASE("Masked eigenCube means use an inclusive good-pixel threshold", "[improc::eigenCube]")
Verify masked mean combinations accept an inclusive good-pixel fraction and use the cube scalar senti...
Eigen::Array< scalarT, -1, -1 > eigenImage
Definition of the eigenImage type, which is an alias for Eigen::Array.
imageT::Scalar imageMedian(const imageT &mat, const maskT *mask, std::vector< typename imageT::Scalar > *work=0)
Calculate the median of an Eigen-like array.
@ sizeerr
A size was invalid or calculated incorrectly.
Definition error_t.hpp:35
@ invalidarg
An argument was invalid.
Definition error_t.hpp:29
bool isInvalidPixel(realT value)
Check whether a value represents an invalid image pixel.