mxlib
c++ tools for analyzing astronomical data and other tasks by Jared R. Males. [git repo]
Loading...
Searching...
No Matches
svdDowndate_test.cpp
Go to the documentation of this file.
1/** \file
2 * \brief Tests reusable thin-SVD row and column deletion updates.
3 *
4 * \ingroup gen_math_test_files
5 */
6
7#include "../../catch2/catch.hpp"
8
10
11#include <Eigen/SVD>
12
13#include <algorithm>
14#include <cmath>
15#include <cstdint>
16#include <limits>
17#include <new>
18#include <stdexcept>
19#include <string>
20#include <tuple>
21#include <type_traits>
22#include <utility>
23#include <vector>
24
25/** \cond */
26namespace
27{
28
29using realT = double;
32using backendT = mx::math::svdDeletionBackend;
33using statusT = mx::math::svdDeletionStatus;
34
35// Deterministic orthonormal columns from the discrete sine transform.
36matrixT sineFactor( Eigen::Index rows, /* [in] number of rows */
37 Eigen::Index cols /* [in] number of columns */ )
38{
39 matrixT factor( rows, cols );
40 const realT scale = std::sqrt( realT( 2 ) / static_cast<realT>( rows + 1 ) );
41 const realT pi = std::acos( realT( -1 ) );
42 for( Eigen::Index row = 0; row < rows; ++row )
43 {
44 for( Eigen::Index column = 0; column < cols; ++column )
45 {
46 factor( row, column ) = scale * std::sin( pi * static_cast<realT>( ( row + 1 ) * ( column + 1 ) ) /
47 static_cast<realT>( rows + 1 ) );
48 }
49 }
50 return factor;
51}
52
53// Return a dynamic identity array.
54matrixT identityMatrix( Eigen::Index size /* [in] square matrix size */ )
55{
56 matrixT identity( size, size );
57 identity.matrix().setIdentity();
58 return identity;
59}
60
61// Construct a represented matrix from thin singular factors.
62matrixT representedMatrix( const matrixT &left, /* [in] thin left factor */
63 const vectorT &values, /* [in] singular values */
64 const matrixT &right /* [in] thin right factor */ )
65{
66 return ( left.matrix() * values.matrix().asDiagonal() * right.matrix().transpose() ).array();
67}
68
69// Physically remove selected rows from a matrix.
70matrixT retainedRows( const matrixT &matrix, /* [in] input matrix */
71 std::span<const Eigen::Index> deleted /* [in] sorted deleted rows */ )
72{
73 matrixT retained( matrix.rows() - static_cast<Eigen::Index>( deleted.size() ), matrix.cols() );
74 Eigen::Index output{ 0 };
75 std::size_t nextDeleted{ 0 };
76 for( Eigen::Index row = 0; row < matrix.rows(); ++row )
77 {
78 if( nextDeleted < deleted.size() && row == deleted[nextDeleted] )
79 {
80 ++nextDeleted;
81 continue;
82 }
83 retained.matrix().row( output++ ) = matrix.matrix().row( row );
84 }
85 return retained;
86}
87
88// Physically remove selected columns from a matrix.
89matrixT retainedColumns( const matrixT &matrix, /* [in] input matrix */
90 std::span<const Eigen::Index> deleted /* [in] sorted deleted columns */ )
91{
92 matrixT retained( matrix.rows(), matrix.cols() - static_cast<Eigen::Index>( deleted.size() ) );
93 Eigen::Index output{ 0 };
94 std::size_t nextDeleted{ 0 };
95 for( Eigen::Index column = 0; column < matrix.cols(); ++column )
96 {
97 if( nextDeleted < deleted.size() && column == deleted[nextDeleted] )
98 {
99 ++nextDeleted;
100 continue;
101 }
102 retained.matrix().col( output++ ) = matrix.matrix().col( column );
103 }
104 return retained;
105}
106
107// Return direct full-SVD singular values padded to the represented rank.
108vectorT directSingularValues( const matrixT &matrix, /* [in] physically retained matrix */
109 Eigen::Index rank /* [in] represented output rank */ )
110{
111 Eigen::JacobiSVD<Eigen::Matrix<realT, Eigen::Dynamic, Eigen::Dynamic>> svd( matrix.matrix(),
112 Eigen::ComputeThinU |
113 Eigen::ComputeThinV );
114 vectorT values( rank );
115 values.setZero();
116 const Eigen::Index copyCount = std::min<Eigen::Index>( rank, svd.singularValues().size() );
117 values.matrix().head( copyCount ) = svd.singularValues().head( copyCount );
118 return values;
119}
120
121// Return the preserved covariance represented by a deletion result.
122matrixT representedCovariance( const matrixT &preservedFactor, /* [in] unchanged thin factor */
123 const mx::math::svdDeletionResult<realT> &result /* [in] deletion result */ )
124{
125 const Eigen::Matrix<realT, Eigen::Dynamic, Eigen::Dynamic> directions =
126 preservedFactor.matrix() * result.rotation().matrix();
127 return ( directions * result.squaredSingularValues().head( result.outputRank() ).matrix().asDiagonal() *
128 directions.transpose() )
129 .array();
130}
131
132// Named diagnostics returned to a TEST_CASE for local Catch2 assertions.
133struct deletionComparison
134{
135 realT squaredSingularError{ 0 };
136 realT squaredSingularTolerance{ 0 };
137 realT singularError{ 0 };
138 realT singularTolerance{ 0 };
139 realT covarianceError{ 0 };
140 realT covarianceTolerance{ 0 };
141};
142
143// Compare all-mode row-deletion outputs with a direct retained-matrix SVD.
144deletionComparison compareRowResult( const matrixT &matrix, /* [in] represented matrix */
145 const matrixT &right, /* [in] unchanged right factor */
146 std::span<const Eigen::Index> deleted, /* [in] deleted rows */
147 const mx::math::svdDeletionResult<realT> &result, /* [in] deletion result */
148 realT tolerance = 5e-11 /* [in] relative tolerance */ )
149{
150 const matrixT retained = retainedRows( matrix, deleted );
151 const vectorT direct = directSingularValues( retained, result.baseRank() );
152 const vectorT directSquared = direct.square();
153 const matrixT expected = ( retained.matrix().transpose() * retained.matrix() ).array();
154 const matrixT actual = representedCovariance( right, result );
155
156 deletionComparison comparison;
157 comparison.squaredSingularError = ( result.squaredSingularValues() - directSquared ).matrix().norm();
158 comparison.squaredSingularTolerance = tolerance * std::max<realT>( 1, directSquared.matrix().norm() );
159 comparison.singularError = ( result.singularValues() - direct ).matrix().norm();
160 comparison.singularTolerance = realT( 16 ) * std::sqrt( std::numeric_limits<realT>::epsilon() ) *
161 std::max<realT>( 1, direct.size() > 0 ? direct( 0 ) : realT( 0 ) );
162 comparison.covarianceError = ( actual - expected ).matrix().norm();
163 comparison.covarianceTolerance = tolerance * std::max<realT>( 1, expected.matrix().norm() );
164 return comparison;
165}
166
167// Compare all-mode column-deletion outputs with a direct retained-matrix SVD.
168deletionComparison compareColumnResult( const matrixT &matrix, /* [in] represented matrix */
169 const matrixT &left, /* [in] unchanged left factor */
170 std::span<const Eigen::Index> deleted, /* [in] deleted columns */
171 const mx::math::svdDeletionResult<realT> &result, /* [in] deletion result */
172 realT tolerance = 5e-11 /* [in] relative tolerance */ )
173{
174 const matrixT retained = retainedColumns( matrix, deleted );
175 const vectorT direct = directSingularValues( retained, result.baseRank() );
176 const vectorT directSquared = direct.square();
177 const matrixT expected = ( retained.matrix() * retained.matrix().transpose() ).array();
178 const matrixT actual = representedCovariance( left, result );
179
180 deletionComparison comparison;
181 comparison.squaredSingularError = ( result.squaredSingularValues() - directSquared ).matrix().norm();
182 comparison.squaredSingularTolerance = tolerance * std::max<realT>( 1, directSquared.matrix().norm() );
183 comparison.singularError = ( result.singularValues() - direct ).matrix().norm();
184 comparison.singularTolerance = realT( 16 ) * std::sqrt( std::numeric_limits<realT>::epsilon() ) *
185 std::max<realT>( 1, direct.size() > 0 ? direct( 0 ) : realT( 0 ) );
186 comparison.covarianceError = ( actual - expected ).matrix().norm();
187 comparison.covarianceTolerance = tolerance * std::max<realT>( 1, expected.matrix().norm() );
188 return comparison;
189}
190
191mx::math::detail::svdDeletionTestOperation failingOperation =
192 mx::math::detail::svdDeletionTestOperation::prepareWorkspace;
193
194// Deterministic behavior selected for an injected solver call.
195enum class solverHookMode
196{
197 production,
198 queryFailure,
199 invalidQuery,
200 invalidIntegerQuery,
201 solveFailure,
202 countMismatch,
203 nonFiniteValue,
204 nonFiniteVector,
205 invalidOrdering,
206 outsideInterlacing,
207 invalidVectorNorm,
208 invalidResidual,
209 negativeSpectrum,
210 tinySpectrum,
211 roundoffClamp,
212 indefinite
213};
214
215solverHookMode syevrMode = solverHookMode::production;
216solverHookMode gesvdMode = solverHookMode::production;
217solverHookMode laed9Mode = solverHookMode::production;
218
219// Throw a deterministic allocation failure for the selected operation.
220void throwAllocation( mx::math::detail::svdDeletionTestOperation operation /* [in] operation being attempted */ )
221{
222 if( operation == failingOperation )
223 {
224 throw std::bad_alloc();
225 }
226}
227
228// Throw a deterministic storage-length failure for the selected operation.
229void throwLengthError( mx::math::detail::svdDeletionTestOperation operation /* [in] operation being attempted */ )
230{
231 if( operation == failingOperation )
232 {
233 throw std::length_error( "injected SVD deletion storage length" );
234 }
235}
236
237// Emulate selected SYEVR query, failure, and malformed-output paths.
238MXLAPACK_INT syevrHook( char jobz, /* [in] eigenvector request */
239 char range, /* [in] eigenvalue selection mode */
240 char uplo, /* [in] populated triangle */
241 MXLAPACK_INT n, /* [in] matrix order */
242 realT *matrix, /* [in,out] input matrix */
243 MXLAPACK_INT lda, /* [in] input leading dimension */
244 realT valueLower, /* [in] lower value bound */
245 realT valueUpper, /* [in] upper value bound */
246 MXLAPACK_INT indexLow, /* [in] first requested index */
247 MXLAPACK_INT indexHigh, /* [in] last requested index */
248 realT tolerance, /* [in] convergence tolerance */
249 MXLAPACK_INT *found, /* [out] returned eigenvalue count */
250 realT *eigenvalues, /* [out] eigenvalues */
251 realT *eigenvectors, /* [out] eigenvectors */
252 MXLAPACK_INT ldz, /* [in] eigenvector leading dimension */
253 MXLAPACK_INT *support, /* [out] eigenvector support */
254 realT *work, /* [in,out] floating workspace */
255 MXLAPACK_INT lwork, /* [in] floating workspace size */
256 MXLAPACK_INT *integerWork, /* [in,out] integer workspace */
257 MXLAPACK_INT integerWorkSize /* [in] integer workspace size */ )
258{
259 static_cast<void>( jobz );
260 static_cast<void>( range );
261 static_cast<void>( uplo );
262 static_cast<void>( matrix );
263 static_cast<void>( lda );
264 static_cast<void>( valueLower );
265 static_cast<void>( valueUpper );
266 static_cast<void>( indexLow );
267 static_cast<void>( indexHigh );
268 static_cast<void>( tolerance );
269 static_cast<void>( support );
270
271 if( lwork == -1 || integerWorkSize == -1 )
272 {
273 if( syevrMode == solverHookMode::queryFailure )
274 {
275 return 61;
276 }
277 work[0] = syevrMode == solverHookMode::invalidQuery ? realT( 0 ) : realT( std::max( 1, 32 * n ) );
278 integerWork[0] = syevrMode == solverHookMode::invalidIntegerQuery ? 0 : std::max<MXLAPACK_INT>( 1, 10 * n );
279 return 0;
280 }
281 if( syevrMode == solverHookMode::solveFailure )
282 {
283 return 71;
284 }
285
286 *found = syevrMode == solverHookMode::countMismatch ? std::max<MXLAPACK_INT>( 0, n - 1 ) : n;
287 for( MXLAPACK_INT column = 0; column < n; ++column )
288 {
289 eigenvalues[column] = static_cast<realT>( column + 1 );
290 for( MXLAPACK_INT row = 0; row < n; ++row )
291 {
292 eigenvectors[row + column * ldz] = row == column ? realT( 1 ) : realT( 0 );
293 }
294 }
295 if( syevrMode == solverHookMode::nonFiniteValue )
296 {
297 eigenvalues[0] = std::numeric_limits<realT>::infinity();
298 }
299 else if( syevrMode == solverHookMode::nonFiniteVector )
300 {
301 eigenvectors[0] = std::numeric_limits<realT>::infinity();
302 }
303 else if( syevrMode == solverHookMode::invalidOrdering && n > 1 )
304 {
305 eigenvalues[0] = realT( 2 );
306 eigenvalues[1] = realT( 1 );
307 }
308 else if( syevrMode == solverHookMode::roundoffClamp )
309 {
310 eigenvalues[0] = -std::numeric_limits<realT>::epsilon();
311 }
312 else if( syevrMode == solverHookMode::indefinite )
313 {
314 eigenvalues[0] = realT( -0.25 );
315 }
316 return 0;
317}
318
319// Emulate selected GESVD query, failure, and malformed-output paths.
320MXLAPACK_INT gesvdHook( char jobu, /* [in] left-vector request */
321 char jobvt, /* [in] right-vector request */
322 MXLAPACK_INT rows, /* [in] matrix row count */
323 MXLAPACK_INT cols, /* [in] matrix column count */
324 realT *matrix, /* [in,out] input matrix */
325 MXLAPACK_INT lda, /* [in] input leading dimension */
326 realT *singular, /* [out] singular values */
327 realT *left, /* [out] left vectors */
328 MXLAPACK_INT ldu, /* [in] left-vector leading dimension */
329 realT *rightTranspose, /* [out] transposed right vectors */
330 MXLAPACK_INT ldvt, /* [in] right-vector leading dimension */
331 realT *work, /* [in,out] floating workspace */
332 MXLAPACK_INT lwork /* [in] floating workspace size */ )
333{
334 static_cast<void>( jobu );
335 static_cast<void>( jobvt );
336 static_cast<void>( rows );
337 static_cast<void>( matrix );
338 static_cast<void>( lda );
339 static_cast<void>( left );
340 static_cast<void>( ldu );
341
342 if( lwork == -1 )
343 {
344 if( gesvdMode == solverHookMode::queryFailure )
345 {
346 return 62;
347 }
348 work[0] = gesvdMode == solverHookMode::invalidQuery ? realT( 0 ) : realT( std::max( 1, 5 * cols ) );
349 return 0;
350 }
351 if( gesvdMode == solverHookMode::solveFailure )
352 {
353 return 72;
354 }
355
356 for( MXLAPACK_INT index = 0; index < cols; ++index )
357 {
358 singular[index] = static_cast<realT>( cols - index );
359 for( MXLAPACK_INT row = 0; row < cols; ++row )
360 {
361 rightTranspose[row + index * ldvt] = row == index ? realT( 1 ) : realT( 0 );
362 }
363 }
364 if( gesvdMode == solverHookMode::nonFiniteValue )
365 {
366 singular[0] = std::numeric_limits<realT>::infinity();
367 }
368 else if( gesvdMode == solverHookMode::nonFiniteVector )
369 {
370 rightTranspose[0] = std::numeric_limits<realT>::infinity();
371 }
372 else if( gesvdMode == solverHookMode::invalidOrdering && cols > 1 )
373 {
374 singular[0] = realT( 1 );
375 singular[1] = realT( 2 );
376 }
377 else if( gesvdMode == solverHookMode::negativeSpectrum )
378 {
379 singular[cols - 1] = realT( -1 );
380 }
381 else if( gesvdMode == solverHookMode::tinySpectrum )
382 {
383 singular[0] = realT( 1e-200 );
384 }
385 return 0;
386}
387
388// Emulate selected LAED9 failure and malformed-output paths.
389MXLAPACK_INT laed9Hook( realT *eigenvalues, /* [out] ascending updated eigenvalues */
390 realT *delta, /* [out] secular workspace */
391 realT *eigenvectors, /* [out] updated eigenvectors */
392 MXLAPACK_INT rank, /* [in] active secular-system dimension */
393 MXLAPACK_INT leadingDimension, /* [in] output leading dimension */
394 realT rho, /* [in] positive rank-one weight */
395 realT *poles, /* [in,out] ascending diagonal poles */
396 realT *update /* [in,out] normalized update */ )
397{
398 static_cast<void>( delta );
399 static_cast<void>( rho );
400 static_cast<void>( update );
401
402 if( laed9Mode == solverHookMode::solveFailure )
403 {
404 return 73;
405 }
406
407 for( MXLAPACK_INT column = 0; column < rank; ++column )
408 {
409 eigenvalues[column] = poles[column];
410 for( MXLAPACK_INT row = 0; row < rank; ++row )
411 {
412 eigenvectors[row + column * leadingDimension] = row == column ? realT( 1 ) : realT( 0 );
413 }
414 }
415 if( laed9Mode == solverHookMode::nonFiniteValue )
416 {
417 eigenvalues[0] = std::numeric_limits<realT>::infinity();
418 }
419 else if( laed9Mode == solverHookMode::nonFiniteVector )
420 {
421 eigenvectors[0] = std::numeric_limits<realT>::infinity();
422 }
423 else if( laed9Mode == solverHookMode::invalidOrdering && rank > 1 )
424 {
425 eigenvalues[0] = realT( 1 );
426 eigenvalues[1] = realT( 0 );
427 }
428 else if( laed9Mode == solverHookMode::outsideInterlacing )
429 {
430 eigenvalues[0] = poles[0] - realT( 1 );
431 }
432 else if( laed9Mode == solverHookMode::invalidVectorNorm )
433 {
434 eigenvectors[0] = realT( 2 );
435 }
436 else if( laed9Mode == solverHookMode::roundoffClamp )
437 {
438 eigenvalues[rank - 1] = std::numeric_limits<realT>::epsilon();
439 }
440 return 0;
441}
442
443// Restore production failure hooks at scope exit.
444class hookGuard
445{
446 public:
447 // Install no hooks at the beginning of a test scope.
448 hookGuard()
449 {
450 mx::math::detail::svdDeletionHooks<double>() = {};
451 mx::math::detail::svdDeletionHooks<float>() = {};
452 syevrMode = solverHookMode::production;
453 gesvdMode = solverHookMode::production;
454 laed9Mode = solverHookMode::production;
455 }
456
457 // Restore production hooks after every test exit path.
458 ~hookGuard()
459 {
460 mx::math::detail::svdDeletionHooks<double>() = {};
461 mx::math::detail::svdDeletionHooks<float>() = {};
462 syevrMode = solverHookMode::production;
463 gesvdMode = solverHookMode::production;
464 laed9Mode = solverHookMode::production;
465 }
466};
467
468} // namespace
469/** \endcond */
470
471namespace unitTest::math_svdDowndate_test
472{
473
474/// SVD row deletion matches direct full SVDs
475/** Verifies that svdRemoveRows reproduces direct full SVDs for complete tall, wide, and square factors.
476 *
477 * \ingroup svdDowndate_unit_tests
478 */
479TEST_CASE( "SVD row deletion matches direct full SVDs", "[math::svdDowndate][rows]" )
480{
481 for( const auto [rows, cols, deleted] :
482 std::vector<std::tuple<Eigen::Index, Eigen::Index, std::vector<Eigen::Index>>>{ { 7, 4, { 0, 5 } },
483 { 4, 7, { 1 } },
484 { 5, 5, { 2 } },
485 { 9, 3, { 0, 2, 4, 6 } } } )
486 {
487 const Eigen::Index rank = std::min( rows, cols );
488 const matrixT left = sineFactor( rows, rank );
489 const matrixT right = sineFactor( cols, rank );
490 vectorT singular( rank );
491 for( Eigen::Index index = 0; index < rank; ++index )
492 {
493 singular( index ) = realT( 9 - 2 * index ) + realT( 0.25 * index );
494 }
495 const matrixT matrix = representedMatrix( left, singular, right );
496
497 for( const backendT backend : { backendT::leadingCovariance, backendT::stableCore } )
498 {
499 CAPTURE( rows, cols, deleted );
500 INFO( "backend: " << mx::math::svdDeletionBackendName( backend ) );
503 const statusT status = mx::math::svdRemoveRows( result, singular, left, deleted, rank, workspace, backend );
504 REQUIRE( mx::math::svdDeletionSucceeded( status ) );
505 REQUIRE( result.backend() == backend );
506 const deletionComparison comparison = compareRowResult( matrix, right, deleted, result );
507 REQUIRE( comparison.squaredSingularError <= comparison.squaredSingularTolerance );
508 REQUIRE( comparison.singularError <= comparison.singularTolerance );
509 REQUIRE( comparison.covarianceError <= comparison.covarianceTolerance );
510 }
511 }
512}
513
514/// SVD column deletion matches direct full SVDs
515/** Verifies that svdRemoveColumns reproduces direct full SVDs and the row-deletion transpose dual.
516 *
517 * \ingroup svdDowndate_unit_tests
518 */
519TEST_CASE( "SVD column deletion matches direct full SVDs", "[math::svdDowndate][columns]" )
520{
521 for( const auto [rows, cols, deleted] :
522 std::vector<std::tuple<Eigen::Index, Eigen::Index, std::vector<Eigen::Index>>>{ { 7, 4, { 1 } },
523 { 4, 7, { 0, 6 } },
524 { 5, 5, { 1, 3 } } } )
525 {
526 const Eigen::Index rank = std::min( rows, cols );
527 const matrixT left = sineFactor( rows, rank );
528 const matrixT right = sineFactor( cols, rank );
529 vectorT singular( rank );
530 for( Eigen::Index index = 0; index < rank; ++index )
531 {
532 singular( index ) = realT( 11 - 2 * index ) + realT( 0.125 * index );
533 }
534 const matrixT matrix = representedMatrix( left, singular, right );
535
536 for( const backendT backend : { backendT::leadingCovariance, backendT::stableCore } )
537 {
538 CAPTURE( rows, cols, deleted );
539 INFO( "backend: " << mx::math::svdDeletionBackendName( backend ) );
542 const statusT status =
543 mx::math::svdRemoveColumns( result, singular, right, deleted, rank, workspace, backend );
544 REQUIRE( mx::math::svdDeletionSucceeded( status ) );
545 const deletionComparison comparison = compareColumnResult( matrix, left, deleted, result );
546 REQUIRE( comparison.squaredSingularError <= comparison.squaredSingularTolerance );
547 REQUIRE( comparison.singularError <= comparison.singularTolerance );
548 REQUIRE( comparison.covarianceError <= comparison.covarianceTolerance );
549 }
550 }
551}
552
553/// SVD deletion core entry points agree
554/** Verifies svdDeletionLeadingCore, svdDeletionStableCore, and svdDeletionCore against the same direct SVD.
555 *
556 * \ingroup svdDowndate_unit_tests
557 */
558TEST_CASE( "SVD deletion core entry points agree", "[math::svdDowndate][core]" )
559{
560 const matrixT left = sineFactor( 7, 4 );
561 const matrixT right = sineFactor( 5, 4 );
562 vectorT singular( 4 );
563 singular << 10, 6, 3, 0.5;
564 const matrixT matrix = representedMatrix( left, singular, right );
565 const std::vector<Eigen::Index> deleted{ 1, 5 };
566 matrixT deletedRows( deleted.size(), left.cols() );
567 for( Eigen::Index row = 0; row < deletedRows.rows(); ++row )
568 {
569 deletedRows.matrix().row( row ) = left.matrix().row( deleted[row] );
570 }
571
575 mx::math::svdDeletionLeadingCore( leadingResult, singular, deletedRows, 4, leadingWorkspace ) ) );
576 const deletionComparison leadingComparison = compareRowResult( matrix, right, deleted, leadingResult );
577 REQUIRE( leadingComparison.squaredSingularError <= leadingComparison.squaredSingularTolerance );
578 REQUIRE( leadingComparison.singularError <= leadingComparison.singularTolerance );
579 REQUIRE( leadingComparison.covarianceError <= leadingComparison.covarianceTolerance );
580
584 mx::math::svdDeletionStableCore( stableResult, singular, deletedRows, 4, stableWorkspace ) ) );
585 const deletionComparison stableComparison = compareRowResult( matrix, right, deleted, stableResult );
586 REQUIRE( stableComparison.squaredSingularError <= stableComparison.squaredSingularTolerance );
587 REQUIRE( stableComparison.singularError <= stableComparison.singularTolerance );
588 REQUIRE( stableComparison.covarianceError <= stableComparison.covarianceTolerance );
589
592 singular,
593 deletedRows,
594 3,
595 stableWorkspace,
596 backendT::stableCore ) ) );
597 REQUIRE( dispatchResult.outputRank() == 3 );
598 REQUIRE( ( dispatchResult.singularValues() - stableResult.singularValues() ).matrix().norm() < 1e-12 );
599 REQUIRE( ( dispatchResult.squaredSingularValues() - stableResult.squaredSingularValues() ).matrix().norm() <
600 1e-12 );
601 REQUIRE( dispatchResult.singularValues().size() == 4 );
602 REQUIRE( dispatchResult.rotation().cols() == 3 );
603
604 const matrixT retained = retainedRows( matrix, deleted );
605 Eigen::JacobiSVD<Eigen::Matrix<realT, Eigen::Dynamic, Eigen::Dynamic>> directSvd( retained.matrix(),
606 Eigen::ComputeThinV );
607 const Eigen::Matrix<realT, Eigen::Dynamic, Eigen::Dynamic> directDirections = directSvd.matrixV().leftCols( 3 );
608 const Eigen::Matrix<realT, Eigen::Dynamic, Eigen::Dynamic> updatedDirections =
609 right.matrix() * dispatchResult.rotation().matrix();
610 const Eigen::Matrix<realT, Eigen::Dynamic, Eigen::Dynamic> directProjector =
611 directDirections * directDirections.transpose();
612 const Eigen::Matrix<realT, Eigen::Dynamic, Eigen::Dynamic> updatedProjector =
613 updatedDirections * updatedDirections.transpose();
614 REQUIRE( ( updatedProjector - directProjector ).norm() < 1e-10 );
615}
616
617/// Rank-one secular SVD deletion matches direct row and column SVDs
618/** Verifies that svdRemoveRows and svdRemoveColumns with the rankOneSecular backend reproduce direct full SVDs
619 * while publishing either the complete eigensystem or a requested leading prefix.
620 *
621 * \ingroup svdDowndate_unit_tests
622 */
623TEST_CASE( "Rank-one secular SVD deletion matches direct row and column SVDs", "[math::svdDowndate][rankOneSecular]" )
624{
625 const Eigen::Index rank{ 4 };
626 const matrixT left = sineFactor( 9, rank );
627 const matrixT right = sineFactor( 7, rank );
628 vectorT singular( rank );
629 singular << 11, 6, 2.5, 0.75;
630 const matrixT matrix = representedMatrix( left, singular, right );
631
632 SECTION( "row deletion" )
633 {
634 const std::vector<Eigen::Index> deleted{ 4 };
637 REQUIRE(
638 mx::math::svdRemoveRows( result, singular, left, deleted, rank, workspace, backendT::rankOneSecular ) ==
639 statusT::success );
640 REQUIRE( result.backend() == backendT::rankOneSecular );
641 const deletionComparison comparison = compareRowResult( matrix, right, deleted, result );
642 REQUIRE( comparison.squaredSingularError <= comparison.squaredSingularTolerance );
643 REQUIRE( comparison.singularError <= comparison.singularTolerance );
644 REQUIRE( comparison.covarianceError <= comparison.covarianceTolerance );
645 }
646
647 SECTION( "column deletion" )
648 {
649 const std::vector<Eigen::Index> deleted{ 2 };
652 REQUIRE(
653 mx::math::svdRemoveColumns( result, singular, right, deleted, rank, workspace, backendT::rankOneSecular ) ==
654 statusT::success );
655 REQUIRE( result.backend() == backendT::rankOneSecular );
656 const deletionComparison comparison = compareColumnResult( matrix, left, deleted, result );
657 REQUIRE( comparison.squaredSingularError <= comparison.squaredSingularTolerance );
658 REQUIRE( comparison.singularError <= comparison.singularTolerance );
659 REQUIRE( comparison.covarianceError <= comparison.covarianceTolerance );
660 }
661
662 SECTION( "leading output prefix" )
663 {
664 constexpr Eigen::Index outputRank{ 2 };
665 const std::vector<Eigen::Index> deleted{ 4 };
666 const matrixT retained = retainedRows( matrix, deleted );
667 Eigen::JacobiSVD<Eigen::Matrix<realT, Eigen::Dynamic, Eigen::Dynamic>> direct( retained.matrix(),
668 Eigen::ComputeThinV );
669
672 REQUIRE( mx::math::svdRemoveRows( result,
673 singular,
674 left,
675 deleted,
676 outputRank,
677 workspace,
678 backendT::rankOneSecular ) == statusT::success );
679 REQUIRE( result.outputRank() == outputRank );
680 REQUIRE( result.rotation().rows() == rank );
681 REQUIRE( result.rotation().cols() == outputRank );
682 REQUIRE( result.singularValues().size() == rank );
683 REQUIRE( ( result.singularValues().head( outputRank ).matrix() - direct.singularValues().head( outputRank ) )
684 .norm() < 5e-11 );
685
686 const Eigen::Matrix<realT, Eigen::Dynamic, Eigen::Dynamic> expectedDirections =
687 direct.matrixV().leftCols( outputRank );
688 const Eigen::Matrix<realT, Eigen::Dynamic, Eigen::Dynamic> actualDirections =
689 right.matrix() * result.rotation().matrix();
690 const Eigen::Matrix<realT, Eigen::Dynamic, Eigen::Dynamic> expectedProjector =
691 expectedDirections * expectedDirections.transpose();
692 const Eigen::Matrix<realT, Eigen::Dynamic, Eigen::Dynamic> actualProjector =
693 actualDirections * actualDirections.transpose();
694 REQUIRE( ( actualProjector - expectedProjector ).norm() < 5e-10 );
695 }
696}
697
698/// Rank-one secular SVD deletion handles deflation and leverage edge cases
699/** Verifies exact and clustered repeated singular values, zero singular values, zero-leverage deletion, and
700 * high-leverage deletion through svdRemoveRows with the rankOneSecular backend.
701 *
702 * \ingroup svdDowndate_unit_tests
703 */
704TEST_CASE( "Rank-one secular SVD deletion handles deflation and leverage edge cases",
705 "[math::svdDowndate][rankOneSecular][conditioning]" )
706{
707 SECTION( "scalar secular system" )
708 {
709 matrixT left( 2, 1 );
710 left << std::sqrt( 0.5 ), std::sqrt( 0.5 );
711 vectorT singular( 1 );
712 singular << std::sqrt( 2.0 );
713 const std::vector<Eigen::Index> deleted{ 0 };
714
717 REQUIRE( mx::math::svdRemoveRows( result, singular, left, deleted, 1, workspace, backendT::rankOneSecular ) ==
718 statusT::success );
719 REQUIRE( result.singularValues()( 0 ) == Approx( 1.0 ).epsilon( 1e-12 ) );
720 REQUIRE( result.squaredSingularValues()( 0 ) == Approx( 1.0 ).epsilon( 1e-12 ) );
721 REQUIRE( std::abs( result.rotation()( 0, 0 ) ) == Approx( 1.0 ).epsilon( 1e-12 ) );
722 }
723
724 SECTION( "exact repeated spectrum" )
725 {
726 const matrixT left = sineFactor( 8, 4 );
727 const matrixT right = sineFactor( 6, 4 );
728 vectorT singular( 4 );
729 singular << 9, 9, 3, 3;
730 const matrixT matrix = representedMatrix( left, singular, right );
731 const std::vector<Eigen::Index> deleted{ 3 };
732
736 mx::math::svdRemoveRows( result, singular, left, deleted, 4, workspace, backendT::rankOneSecular ) ) );
737 const deletionComparison comparison = compareRowResult( matrix, right, deleted, result, 2e-10 );
738 REQUIRE( comparison.squaredSingularError <= comparison.squaredSingularTolerance );
739 REQUIRE( comparison.singularError <= comparison.singularTolerance );
740 REQUIRE( comparison.covarianceError <= comparison.covarianceTolerance );
741 }
742
743 SECTION( "clustered spectrum" )
744 {
745 const matrixT left = sineFactor( 9, 4 );
746 const matrixT right = sineFactor( 7, 4 );
747 const realT spacing = realT( 8 ) * std::numeric_limits<realT>::epsilon();
748 vectorT singular( 4 );
749 singular << 10, 10 * ( 1 - spacing ), 2, 2 * ( 1 - spacing );
750 const matrixT matrix = representedMatrix( left, singular, right );
751 const std::vector<Eigen::Index> deleted{ 5 };
752
756 mx::math::svdRemoveRows( result, singular, left, deleted, 4, workspace, backendT::rankOneSecular ) ) );
757 const deletionComparison comparison = compareRowResult( matrix, right, deleted, result, 5e-10 );
758 REQUIRE( comparison.squaredSingularError <= comparison.squaredSingularTolerance );
759 REQUIRE( comparison.singularError <= comparison.singularTolerance );
760 REQUIRE( comparison.covarianceError <= comparison.covarianceTolerance );
761 }
762
763 SECTION( "zero singular spectrum" )
764 {
765 const matrixT left = sineFactor( 8, 4 );
766 const matrixT right = sineFactor( 6, 4 );
767 vectorT singular( 4 );
768 singular << 8, 3, 0, 0;
769 const matrixT matrix = representedMatrix( left, singular, right );
770 const std::vector<Eigen::Index> deleted{ 2 };
771
775 mx::math::svdRemoveRows( result, singular, left, deleted, 4, workspace, backendT::rankOneSecular ) ) );
776 const deletionComparison comparison = compareRowResult( matrix, right, deleted, result, 2e-10 );
777 REQUIRE( comparison.squaredSingularError <= comparison.squaredSingularTolerance );
778 REQUIRE( comparison.singularError <= comparison.singularTolerance );
779 REQUIRE( comparison.covarianceError <= comparison.covarianceTolerance );
780 }
781
782 SECTION( "zero leverage" )
783 {
784 matrixT left( 5, 4 );
785 left.setZero();
786 left.matrix().topRows( 4 ).setIdentity();
787 const matrixT right = sineFactor( 6, 4 );
788 vectorT singular( 4 );
789 singular << 8, 4, 2, 1;
790 const matrixT matrix = representedMatrix( left, singular, right );
791 const std::vector<Eigen::Index> deleted{ 4 };
792
796 mx::math::svdRemoveRows( result, singular, left, deleted, 4, workspace, backendT::rankOneSecular ) ) );
797 const deletionComparison comparison = compareRowResult( matrix, right, deleted, result );
798 REQUIRE( comparison.squaredSingularError <= comparison.squaredSingularTolerance );
799 REQUIRE( comparison.singularError <= comparison.singularTolerance );
800 REQUIRE( comparison.covarianceError <= comparison.covarianceTolerance );
801 }
802
803 SECTION( "high leverage" )
804 {
805 matrixT left( 6, 3 );
806 left.setZero();
807 const realT residualLeverage = 1e-10;
808 left( 0, 0 ) = std::sqrt( 1 - residualLeverage );
809 left( 3, 0 ) = std::sqrt( residualLeverage );
810 left( 1, 1 ) = std::sqrt( 0.75 );
811 left( 4, 1 ) = 0.5;
812 left( 2, 2 ) = std::sqrt( 0.6 );
813 left( 5, 2 ) = std::sqrt( 0.4 );
814 const matrixT right = sineFactor( 5, 3 );
815 vectorT singular( 3 );
816 singular << 10, 4, 1;
817 const matrixT matrix = representedMatrix( left, singular, right );
818 const std::vector<Eigen::Index> deleted{ 0 };
819
820 REQUIRE( mx::math::validateSvdDeletionFactor( left ) == statusT::success );
824 mx::math::svdRemoveRows( result, singular, left, deleted, 3, workspace, backendT::rankOneSecular ) ) );
825 const deletionComparison comparison = compareRowResult( matrix, right, deleted, result, 5e-10 );
826 REQUIRE( comparison.squaredSingularError <= comparison.squaredSingularTolerance );
827 REQUIRE( comparison.singularError <= comparison.singularTolerance );
828 REQUIRE( comparison.covarianceError <= comparison.covarianceTolerance );
829 }
830}
831
832/// Rank-one secular SVD deletion supports float and enforces its deletion contract
833/** Verifies float accuracy, the empty identity path, one-row-only rejection in svdDeletionCore and svdRemoveRows,
834 * workspace capacity validation, and stable backend/status names for rankOneSecular.
835 *
836 * \ingroup svdDowndate_unit_tests
837 */
838TEST_CASE( "Rank-one secular SVD deletion supports float and enforces its deletion contract",
839 "[math::svdDowndate][rankOneSecular][float][errors]" )
840{
841 hookGuard guard;
842 REQUIRE( std::string( mx::math::svdDeletionBackendName( backendT::rankOneSecular ) ) == "rankOneSecular" );
843 REQUIRE( std::string( mx::math::svdDeletionStatusName( statusT::unsupportedDeletionCount ) ) ==
844 "unsupportedDeletionCount" );
845
846 SECTION( "float row deletion" )
847 {
848 using floatMatrixT = mx::math::svdDeletionMatrix<float>;
849 using floatVectorT = mx::math::svdDeletionVector<float>;
850 const floatMatrixT left = sineFactor( 7, 3 ).cast<float>();
851 const floatMatrixT right = sineFactor( 5, 3 ).cast<float>();
852 floatVectorT singular( 3 );
853 singular << 7, 3, 0.5F;
854 const Eigen::MatrixXf matrix = left.matrix() * singular.matrix().asDiagonal() * right.matrix().transpose();
855 Eigen::MatrixXf retained( 6, 5 );
856 retained.topRows( 2 ) = matrix.topRows( 2 );
857 retained.bottomRows( 4 ) = matrix.bottomRows( 4 );
858 Eigen::JacobiSVD<Eigen::MatrixXf> direct( retained, Eigen::ComputeThinV );
859 const std::vector<Eigen::Index> deleted{ 2 };
860
864 mx::math::svdRemoveRows( result, singular, left, deleted, 3, workspace, backendT::rankOneSecular ) ) );
865 REQUIRE( result.backend() == backendT::rankOneSecular );
866 REQUIRE( ( result.singularValues().matrix() - direct.singularValues().head( 3 ) ).norm() < 5e-4F );
867 }
868
869 SECTION( "empty deletion is identity" )
870 {
871 vectorT singular( 3 );
872 singular << 7, 2, 0.5;
873 const matrixT factor = identityMatrix( 3 );
874 const std::vector<Eigen::Index> deleted;
877
878 REQUIRE( mx::math::svdRemoveRows( result, singular, factor, deleted, 2, workspace, backendT::rankOneSecular ) ==
879 statusT::success );
880 REQUIRE( result.backend() == backendT::rankOneSecular );
881 REQUIRE( result.outputRank() == 2 );
882 REQUIRE( ( result.singularValues() - singular ).matrix().norm() == Approx( 0.0 ) );
883 REQUIRE( ( result.rotation() - identityMatrix( 3 ).leftCols( 2 ) ).matrix().norm() == Approx( 0.0 ) );
884 REQUIRE( result.minimumPSDValue() == Approx( 0.25 / 49.0 ) );
885 }
886
887 SECTION( "direct empty deletion is identity" )
888 {
889 vectorT singular( 3 );
890 singular << 7, 2, 0.5;
891 matrixT deletedRows( 0, 3 );
894
895 REQUIRE( mx::math::svdDeletionCore( result, singular, deletedRows, 2, workspace, backendT::rankOneSecular ) ==
896 statusT::success );
897 REQUIRE( result.backend() == backendT::rankOneSecular );
898 REQUIRE( ( result.singularValues() - singular ).matrix().norm() == Approx( 0.0 ) );
899 REQUIRE( ( result.rotation() - identityMatrix( 3 ).leftCols( 2 ) ).matrix().norm() == Approx( 0.0 ) );
900 }
901
902 SECTION( "zero spectrum returns an arbitrary identity basis" )
903 {
904 vectorT singular = vectorT::Zero( 2 );
905 matrixT deletedRows( 1, 2 );
906 deletedRows << 0.25, 0.5;
909
910 REQUIRE( mx::math::svdDeletionCore( result, singular, deletedRows, 2, workspace, backendT::rankOneSecular ) ==
911 statusT::success );
912 REQUIRE( result.singularValues().matrix().norm() == Approx( 0.0 ) );
913 REQUIRE( ( result.rotation() - identityMatrix( 2 ) ).matrix().norm() == Approx( 0.0 ) );
914 }
915
916 SECTION( "tiny update is treated as identity" )
917 {
918 vectorT singular( 2 );
919 singular << 1, 0.5;
920 matrixT deletedRows( 1, 2 );
921 deletedRows << 1e-200, 0;
924
925 REQUIRE( mx::math::svdDeletionCore( result, singular, deletedRows, 2, workspace, backendT::rankOneSecular ) ==
926 statusT::success );
927 REQUIRE( ( result.singularValues() - singular ).matrix().norm() == Approx( 0.0 ) );
928 REQUIRE( ( result.rotation() - identityMatrix( 2 ) ).matrix().norm() == Approx( 0.0 ) );
929 }
930
931 SECTION( "invalid direct core shape is rejected" )
932 {
933 vectorT singular( 2 );
934 singular << 3, 1;
935 matrixT deletedRows( 1, 1 );
936 deletedRows << 0.25;
939
940 REQUIRE( mx::math::svdDeletionCore( result, singular, deletedRows, 2, workspace, backendT::rankOneSecular ) ==
941 statusT::invalidInput );
942 REQUIRE( result.status() == statusT::invalidInput );
943 }
944
945 SECTION( "result preparation failure is propagated" )
946 {
947 vectorT singular( 2 );
948 singular << 3, 1;
949 matrixT deletedRows( 1, 2 );
950 deletedRows << 0.25, 0.1;
953
954 failingOperation = mx::math::detail::svdDeletionTestOperation::prepareResult;
955 mx::math::detail::svdDeletionHooks<double>().operation = throwAllocation;
956 REQUIRE( mx::math::svdDeletionCore( result, singular, deletedRows, 2, workspace, backendT::rankOneSecular ) ==
957 statusT::allocationFailure );
958 REQUIRE( result.status() == statusT::allocationFailure );
959 }
960
961 SECTION( "multiple deletions are rejected" )
962 {
963 const matrixT factor = sineFactor( 6, 3 );
964 vectorT singular( 3 );
965 singular << 7, 3, 1;
966 const std::vector<Eigen::Index> deleted{ 1, 4 };
967 matrixT deletedRows( 2, 3 );
968 deletedRows.matrix().row( 0 ) = factor.matrix().row( 1 );
969 deletedRows.matrix().row( 1 ) = factor.matrix().row( 4 );
972
973 REQUIRE( workspace.prepare( 3, 2, backendT::rankOneSecular ) == statusT::unsupportedDeletionCount );
974 REQUIRE( !workspace.prepared() );
975 REQUIRE( mx::math::svdDeletionCore( result, singular, deletedRows, 3, workspace, backendT::rankOneSecular ) ==
976 statusT::unsupportedDeletionCount );
977 REQUIRE( result.status() == statusT::unsupportedDeletionCount );
978 REQUIRE( mx::math::svdRemoveRows( result, singular, factor, deleted, 3, workspace, backendT::rankOneSecular ) ==
979 statusT::unsupportedDeletionCount );
980 REQUIRE( result.status() == statusT::unsupportedDeletionCount );
981 }
982
983 SECTION( "finite oversized update is rejected before squaring" )
984 {
985 vectorT singular( 2 );
986 singular << 1, 0.5;
987 matrixT deletedRows( 1, 2 );
988 deletedRows << std::numeric_limits<realT>::max() / 4, std::numeric_limits<realT>::max() / 4;
991
992 REQUIRE( mx::math::svdDeletionCore( result, singular, deletedRows, 2, workspace, backendT::rankOneSecular ) ==
993 statusT::invalidInput );
994 REQUIRE( result.status() == statusT::invalidInput );
995 }
996
997 SECTION( "materially indefinite core is rejected" )
998 {
999 vectorT singular( 2 );
1000 singular << 1, 0.5;
1001 matrixT deletedRows( 1, 2 );
1002 deletedRows << 2, 0;
1005
1006 REQUIRE( mx::math::svdDeletionCore( result, singular, deletedRows, 2, workspace, backendT::rankOneSecular ) ==
1007 statusT::nonPositiveSemidefinite );
1008 REQUIRE( result.minimumPSDValue() == Approx( -3.0 ) );
1009 }
1010
1011 SECTION( "finite result that cannot be rescaled is rejected" )
1012 {
1013 const realT scale = realT( 2 ) * std::sqrt( std::numeric_limits<realT>::max() );
1014 vectorT singular( 2 );
1015 singular << scale, scale / realT( 2 );
1016 matrixT deletedRows( 1, 2 );
1017 deletedRows << 0.25, 0;
1020
1021 REQUIRE( mx::math::svdDeletionCore( result, singular, deletedRows, 2, workspace, backendT::rankOneSecular ) ==
1022 statusT::rescalingOverflow );
1023 REQUIRE( result.status() == statusT::rescalingOverflow );
1024 }
1025}
1026
1027/// Rank-one secular SVD deletion reports solver failures
1028/** Verifies LAED9 failure, non-finite output, invalid ordering and interlacing, vector norm and residual validation,
1029 * and roundoff clamping through svdDeletionCore with the rankOneSecular backend.
1030 *
1031 * \ingroup svdDowndate_unit_tests
1032 */
1033TEST_CASE( "Rank-one secular SVD deletion reports solver failures",
1034 "[math::svdDowndate][rankOneSecular][errors][solver]" )
1035{
1036 hookGuard guard;
1037 vectorT singular( 3 );
1038 singular << 8, 4, 2;
1039 matrixT deleted( 1, 3 );
1040 deleted << 0.25, 0.2, 0.1;
1041
1042 SECTION( "LAED9 solve failure" )
1043 {
1046 laed9Mode = solverHookMode::solveFailure;
1047 mx::math::detail::svdDeletionHooks<double>().laed9 = laed9Hook;
1048 REQUIRE( mx::math::svdDeletionCore( result, singular, deleted, 3, workspace, backendT::rankOneSecular ) ==
1049 statusT::solverFailure );
1050 REQUIRE( result.lapackInfo() == 73 );
1051 }
1052
1053 SECTION( "LAED9 non-finite values and vectors" )
1054 {
1055 for( const solverHookMode mode : { solverHookMode::nonFiniteValue, solverHookMode::nonFiniteVector } )
1056 {
1057 CAPTURE( static_cast<int>( mode ) );
1060 laed9Mode = mode;
1061 mx::math::detail::svdDeletionHooks<double>().laed9 = laed9Hook;
1062 REQUIRE( mx::math::svdDeletionCore( result, singular, deleted, 3, workspace, backendT::rankOneSecular ) ==
1063 statusT::nonFiniteOutput );
1064 }
1065 }
1066
1067 SECTION( "LAED9 invalid ordering" )
1068 {
1071 laed9Mode = solverHookMode::invalidOrdering;
1072 mx::math::detail::svdDeletionHooks<double>().laed9 = laed9Hook;
1073 REQUIRE( mx::math::svdDeletionCore( result, singular, deleted, 3, workspace, backendT::rankOneSecular ) ==
1074 statusT::invalidSolverOutput );
1075 }
1076
1077 SECTION( "LAED9 result outside secular interlacing bounds" )
1078 {
1081 laed9Mode = solverHookMode::outsideInterlacing;
1082 mx::math::detail::svdDeletionHooks<double>().laed9 = laed9Hook;
1083 REQUIRE( mx::math::svdDeletionCore( result, singular, deleted, 3, workspace, backendT::rankOneSecular ) ==
1084 statusT::invalidSolverOutput );
1085 }
1086
1087 SECTION( "LAED9 non-unit eigenvector" )
1088 {
1091 laed9Mode = solverHookMode::invalidVectorNorm;
1092 mx::math::detail::svdDeletionHooks<double>().laed9 = laed9Hook;
1093 REQUIRE( mx::math::svdDeletionCore( result, singular, deleted, 3, workspace, backendT::rankOneSecular ) ==
1094 statusT::invalidSolverOutput );
1095 }
1096
1097 SECTION( "LAED9 eigenvector with a large matrix-free residual" )
1098 {
1101 laed9Mode = solverHookMode::invalidResidual;
1102 mx::math::detail::svdDeletionHooks<double>().laed9 = laed9Hook;
1103 REQUIRE( mx::math::svdDeletionCore( result, singular, deleted, 3, workspace, backendT::rankOneSecular ) ==
1104 statusT::invalidSolverOutput );
1105 }
1106
1107 SECTION( "small positive roundoff eigenvalue is clamped" )
1108 {
1109 vectorT clampSingular( 2 );
1110 clampSingular << 1, 0.5;
1111 matrixT clampDeleted( 1, 2 );
1112 clampDeleted << 1, 0;
1115 laed9Mode = solverHookMode::roundoffClamp;
1116 mx::math::detail::svdDeletionHooks<double>().laed9 = laed9Hook;
1117
1118 REQUIRE(
1119 mx::math::svdDeletionCore( result, clampSingular, clampDeleted, 1, workspace, backendT::rankOneSecular ) ==
1120 statusT::successWithClamping );
1121 REQUIRE( result.clampedEigenvalues() == 1 );
1122 REQUIRE( result.squaredSingularValues()( 1 ) == Approx( 0.0 ) );
1123 }
1124}
1125
1126/// SVD deletion preserves the projected-factor contract
1127/** Verifies that truncated-factor deletion matches a direct SVD of the represented low-rank matrix, not the
1128 * original full-rank matrix, through svdRemoveRows and svdRemoveColumns.
1129 *
1130 * \ingroup svdDowndate_unit_tests
1131 */
1132TEST_CASE( "SVD deletion preserves the projected-factor contract", "[math::svdDowndate][projected]" )
1133{
1134 const matrixT fullLeft = sineFactor( 7, 5 );
1135 const matrixT fullRight = sineFactor( 5, 5 );
1136 vectorT fullSingular( 5 );
1137 fullSingular << 10, 8, 6, 5, 4;
1138 const matrixT full = representedMatrix( fullLeft, fullSingular, fullRight );
1139
1140 const Eigen::Index rank = 3;
1141 const matrixT left = fullLeft.matrix().leftCols( rank ).array();
1142 const matrixT right = fullRight.matrix().leftCols( rank ).array();
1143 const vectorT singular = fullSingular.head( rank );
1144 const matrixT projected = representedMatrix( left, singular, right );
1145
1146 const std::vector<Eigen::Index> deletedRows{ 1, 5 };
1150 mx::math::svdRemoveRows( rowResult, singular, left, deletedRows, rank, rowWorkspace, backendT::stableCore ) ) );
1151 const deletionComparison rowComparison = compareRowResult( projected, right, deletedRows, rowResult );
1152 REQUIRE( rowComparison.squaredSingularError <= rowComparison.squaredSingularTolerance );
1153 REQUIRE( rowComparison.singularError <= rowComparison.singularTolerance );
1154 REQUIRE( rowComparison.covarianceError <= rowComparison.covarianceTolerance );
1155 const vectorT projectedRowValues = directSingularValues( retainedRows( projected, deletedRows ), rank );
1156 const vectorT fullRowValues = directSingularValues( retainedRows( full, deletedRows ), rank );
1157 REQUIRE( ( projectedRowValues - fullRowValues ).matrix().norm() > 0.1 );
1158
1159 const std::vector<Eigen::Index> deletedColumns{ 0, 4 };
1163 singular,
1164 right,
1165 deletedColumns,
1166 rank,
1167 columnWorkspace,
1168 backendT::leadingCovariance ) ) );
1169 const deletionComparison columnComparison = compareColumnResult( projected, left, deletedColumns, columnResult );
1170 REQUIRE( columnComparison.squaredSingularError <= columnComparison.squaredSingularTolerance );
1171 REQUIRE( columnComparison.singularError <= columnComparison.singularTolerance );
1172 REQUIRE( columnComparison.covarianceError <= columnComparison.covarianceTolerance );
1173 const vectorT projectedColumnValues = directSingularValues( retainedColumns( projected, deletedColumns ), rank );
1174 const vectorT fullColumnValues = directSingularValues( retainedColumns( full, deletedColumns ), rank );
1175 REQUIRE( ( projectedColumnValues - fullColumnValues ).matrix().norm() > 0.1 );
1176}
1177
1178/// SVD deletion handles repeated and high-leverage systems
1179/** Verifies repeated spectra, high-leverage deletion, and the one-row exact deletion invariant through
1180 * svdRemoveRows without comparing ambiguous individual singular vectors.
1181 *
1182 * \ingroup svdDowndate_unit_tests
1183 */
1184TEST_CASE( "SVD deletion handles repeated and high-leverage systems", "[math::svdDowndate][conditioning]" )
1185{
1186 SECTION( "repeated spectrum" )
1187 {
1188 const matrixT left = sineFactor( 8, 4 );
1189 const matrixT right = sineFactor( 6, 4 );
1190 vectorT singular( 4 );
1191 singular << 9, 9, 3, 3;
1192 const matrixT matrix = representedMatrix( left, singular, right );
1193 const std::vector<Eigen::Index> deleted{ 0, 4, 7 };
1194
1195 for( const backendT backend : { backendT::leadingCovariance, backendT::stableCore } )
1196 {
1197 INFO( "backend: " << mx::math::svdDeletionBackendName( backend ) );
1201 mx::math::svdRemoveRows( result, singular, left, deleted, 4, workspace, backend ) ) );
1202 const deletionComparison comparison = compareRowResult( matrix, right, deleted, result );
1203 REQUIRE( comparison.squaredSingularError <= comparison.squaredSingularTolerance );
1204 REQUIRE( comparison.singularError <= comparison.singularTolerance );
1205 REQUIRE( comparison.covarianceError <= comparison.covarianceTolerance );
1206 }
1207 }
1208
1209 SECTION( "high leverage" )
1210 {
1211 matrixT left( 6, 3 );
1212 left.setZero();
1213 const realT residualLeverage = 1e-8;
1214 left( 0, 0 ) = std::sqrt( 1 - residualLeverage );
1215 left( 3, 0 ) = std::sqrt( residualLeverage );
1216 left( 1, 1 ) = std::sqrt( 0.75 );
1217 left( 4, 1 ) = 0.5;
1218 left( 2, 2 ) = std::sqrt( 0.6 );
1219 left( 5, 2 ) = std::sqrt( 0.4 );
1220 const matrixT right = sineFactor( 5, 3 );
1221 vectorT singular( 3 );
1222 singular << 10, 4, 1;
1223 const matrixT matrix = representedMatrix( left, singular, right );
1224 const std::vector<Eigen::Index> deleted{ 0, 1 };
1225
1226 REQUIRE( mx::math::validateSvdDeletionFactor( left ) == statusT::success );
1227 for( const backendT backend : { backendT::leadingCovariance, backendT::stableCore } )
1228 {
1229 INFO( "backend: " << mx::math::svdDeletionBackendName( backend ) );
1233 mx::math::svdRemoveRows( result, singular, left, deleted, 3, workspace, backend ) ) );
1234 const deletionComparison comparison = compareRowResult( matrix, right, deleted, result, 2e-10 );
1235 REQUIRE( comparison.squaredSingularError <= comparison.squaredSingularTolerance );
1236 REQUIRE( comparison.singularError <= comparison.singularTolerance );
1237 REQUIRE( comparison.covarianceError <= comparison.covarianceTolerance );
1238 }
1239 }
1240
1241 SECTION( "one-row deletion" )
1242 {
1243 matrixT left( 2, 1 );
1244 left << std::sqrt( 0.5 ), std::sqrt( 0.5 );
1245 vectorT singular( 1 );
1246 singular << std::sqrt( 2.0 );
1247 const std::vector<Eigen::Index> deleted{ 0 };
1248
1249 for( const backendT backend : { backendT::leadingCovariance, backendT::stableCore } )
1250 {
1254 mx::math::svdRemoveRows( result, singular, left, deleted, 1, workspace, backend ) ) );
1255 REQUIRE( result.singularValues()( 0 ) == Approx( 1.0 ).epsilon( 1e-12 ) );
1256 REQUIRE( result.squaredSingularValues()( 0 ) == Approx( 1.0 ).epsilon( 1e-12 ) );
1257 }
1258
1259 const realT literalLongMalesValue = singular( 0 ) * ( realT( 1 ) - left( 0, 0 ) * left( 0, 0 ) );
1260 REQUIRE( literalLongMalesValue == Approx( realT( 1 ) / std::sqrt( realT( 2 ) ) ).epsilon( 1e-12 ) );
1261 REQUIRE( std::abs( literalLongMalesValue - realT( 1 ) ) > 0.25 );
1262 }
1263
1264 SECTION( "square deleted-side factor" )
1265 {
1266 const matrixT left = identityMatrix( 2 );
1267 vectorT singular( 2 );
1268 singular << 3, 1;
1269 const std::vector<Eigen::Index> deleted{ 0 };
1270 matrixT literalLongMalesCore = identityMatrix( 2 );
1271 literalLongMalesCore.matrix().noalias() -= left.matrix().row( 0 ).transpose() * left.matrix().row( 0 );
1272 literalLongMalesCore.matrix() = singular.matrix().asDiagonal() * literalLongMalesCore.matrix();
1273 const vectorT literalLongMalesValues = directSingularValues( literalLongMalesCore, 2 );
1274
1278 mx::math::svdRemoveRows( result, singular, left, deleted, 2, workspace, backendT::stableCore ) ) );
1279 REQUIRE( ( result.singularValues() - literalLongMalesValues ).matrix().norm() < 1e-12 );
1280 }
1281}
1282
1283/// SVD deletion is invariant across finite scales
1284/** Verifies exponent-safe scale invariance of svdRemoveRows for very large and very small finite spectra.
1285 *
1286 * \ingroup svdDowndate_unit_tests
1287 */
1288TEST_CASE( "SVD deletion is invariant across finite scales", "[math::svdDowndate][scaling]" )
1289{
1290 const matrixT left = sineFactor( 7, 3 );
1291 vectorT baseSingular( 3 );
1292 baseSingular << 8, 3, 1;
1293 const std::vector<Eigen::Index> deleted{ 1, 5 };
1294
1295 for( const backendT backend : { backendT::leadingCovariance, backendT::stableCore } )
1296 {
1298 mx::math::svdDeletionWorkspace<realT> referenceWorkspace;
1300 mx::math::svdRemoveRows( reference, baseSingular, left, deleted, 3, referenceWorkspace, backend ) ) );
1301
1302 for( const realT scale : { realT( 1e140 ), realT( 1e-140 ) } )
1303 {
1304 const vectorT scaledSingular = baseSingular * scale;
1308 mx::math::svdRemoveRows( scaled, scaledSingular, left, deleted, 3, workspace, backend ) ) );
1309 for( Eigen::Index index = 0; index < 3; ++index )
1310 {
1311 REQUIRE( scaled.singularValues()( index ) ==
1312 Approx( reference.singularValues()( index ) * scale ).epsilon( 5e-11 ) );
1313 REQUIRE( scaled.squaredSingularValues()( index ) ==
1314 Approx( reference.squaredSingularValues()( index ) * scale * scale ).epsilon( 1e-10 ) );
1315 }
1316 }
1317 }
1318}
1319
1320/// SVD deletion handles structural and numerical edge cases
1321/** Verifies rank loss, repeated spectra, zero singular values, empty deletion, and factor validation through
1322 * the public SVD deletion APIs.
1323 *
1324 * \ingroup svdDowndate_unit_tests
1325 */
1326TEST_CASE( "SVD deletion handles structural and numerical edge cases", "[math::svdDowndate][rank]" )
1327{
1328 matrixT left( 3, 2 );
1329 left << 1, 0, 0, std::sqrt( 0.5 ), 0, std::sqrt( 0.5 );
1330 const matrixT right = identityMatrix( 2 );
1331 vectorT singular( 2 );
1332 singular << 7, 2;
1333 const std::vector<Eigen::Index> deleted{ 0 };
1334
1335 REQUIRE( mx::math::validateSvdDeletionFactor( left ) == statusT::success );
1336 matrixT invalidFactor = left;
1337 invalidFactor( 1, 1 ) *= 2;
1338 REQUIRE( mx::math::validateSvdDeletionFactor( invalidFactor ) == statusT::factorNotOrthonormal );
1339
1342 for( const backendT backend : { backendT::leadingCovariance, backendT::stableCore } )
1343 {
1345 mx::math::svdRemoveRows( result, singular, left, deleted, 2, workspace, backend ) ) );
1346 REQUIRE( result.squaredSingularValues()( 0 ) == Approx( 4.0 ) );
1347 REQUIRE( result.squaredSingularValues()( 1 ) == Approx( 0.0 ).margin( 1e-12 ) );
1348 }
1349
1350 const std::vector<Eigen::Index> noDeletion;
1351 REQUIRE( mx::math::svdRemoveColumns( result, singular, right, noDeletion, 2, workspace, backendT::stableCore ) ==
1352 statusT::success );
1353 REQUIRE( ( result.rotation() - identityMatrix( 2 ) ).matrix().norm() == Approx( 0.0 ) );
1354 REQUIRE( result.minimumPSDValue() == Approx( 1.0 ) );
1355
1356 matrixT noDeletedRows( 0, 2 );
1357 REQUIRE( mx::math::svdDeletionLeadingCore( result, singular, noDeletedRows, 2, workspace ) == statusT::success );
1358 REQUIRE( result.minimumPSDValue() == Approx( 4.0 / 49.0 ) );
1359 REQUIRE( mx::math::svdDeletionStableCore( result, singular, noDeletedRows, 2, workspace ) == statusT::success );
1360 REQUIRE( result.minimumPSDValue() == Approx( 1.0 ) );
1361
1362 vectorT zeroSingular( 2 );
1363 zeroSingular << 5, 0;
1364 matrixT deletedRow( 1, 2 );
1365 deletedRow << std::sqrt( 0.5 ), std::sqrt( 0.5 );
1367 mx::math::svdDeletionCore( result, zeroSingular, deletedRow, 2, workspace, backendT::stableCore ) ) );
1368 REQUIRE( result.singularValues()( 1 ) == Approx( 0.0 ).margin( 1e-13 ) );
1369
1370 vectorT allZero( 2 );
1371 allZero.setZero();
1372 for( const backendT backend : { backendT::leadingCovariance, backendT::stableCore } )
1373 {
1375 mx::math::svdDeletionCore( result, allZero, deletedRow, 2, workspace, backend ) ) );
1376 REQUIRE( result.singularValues().matrix().norm() == Approx( 0.0 ) );
1377 REQUIRE( ( result.rotation() - identityMatrix( 2 ) ).matrix().norm() == Approx( 0.0 ) );
1378 }
1379
1380 matrixT zeroLeverageFactor( 3, 2 );
1381 zeroLeverageFactor << 1, 0, 0, 1, 0, 0;
1382 vectorT zeroLeverageSingular( 2 );
1383 zeroLeverageSingular << 2, 1;
1384 const matrixT zeroLeverageMatrix =
1385 representedMatrix( zeroLeverageFactor, zeroLeverageSingular, identityMatrix( 2 ) );
1386 const std::vector<Eigen::Index> zeroLeverageDeletion{ 2 };
1387 for( const backendT backend : { backendT::leadingCovariance, backendT::stableCore } )
1388 {
1389 INFO( "backend: " << mx::math::svdDeletionBackendName( backend ) );
1390 mx::math::svdDeletionResult<realT> zeroLeverageResult;
1391 mx::math::svdDeletionWorkspace<realT> zeroLeverageWorkspace;
1392 REQUIRE( mx::math::svdDeletionSucceeded( mx::math::svdRemoveRows( zeroLeverageResult,
1393 zeroLeverageSingular,
1394 zeroLeverageFactor,
1395 zeroLeverageDeletion,
1396 2,
1397 zeroLeverageWorkspace,
1398 backend ) ) );
1399 const deletionComparison comparison =
1400 compareRowResult( zeroLeverageMatrix, identityMatrix( 2 ), zeroLeverageDeletion, zeroLeverageResult );
1401 REQUIRE( comparison.squaredSingularError <= comparison.squaredSingularTolerance );
1402 REQUIRE( comparison.singularError <= comparison.singularTolerance );
1403 REQUIRE( comparison.covarianceError <= comparison.covarianceTolerance );
1404 }
1405}
1406
1407/// SVD deletion supports float and workspace reuse
1408/** Verifies float specialization accuracy and reusable result/workspace capacity through svdDeletionCore.
1409 *
1410 * \ingroup svdDowndate_unit_tests
1411 */
1412TEST_CASE( "SVD deletion supports float and workspace reuse", "[math::svdDowndate][float][workspace]" )
1413{
1414 STATIC_REQUIRE( !std::is_copy_constructible_v<mx::math::svdDeletionResult<float>> );
1415 STATIC_REQUIRE( !std::is_copy_assignable_v<mx::math::svdDeletionResult<float>> );
1416 STATIC_REQUIRE( std::is_nothrow_move_constructible_v<mx::math::svdDeletionResult<float>> );
1417 STATIC_REQUIRE( std::is_nothrow_move_assignable_v<mx::math::svdDeletionResult<float>> );
1418
1419 using floatMatrixT = mx::math::svdDeletionMatrix<float>;
1420 using floatVectorT = mx::math::svdDeletionVector<float>;
1421 floatVectorT singular( 3 );
1422 singular << 6, 3, 1;
1423 floatMatrixT deleted( 1, 3 );
1424 deleted << 0.5f, 0.25f, 0.125f;
1425
1428 REQUIRE( workspace.prepare( 3, 4, backendT::leadingCovariance ) == statusT::success );
1429 REQUIRE( mx::math::svdDeletionCore( result, singular, deleted, 3, workspace, backendT::leadingCovariance ) ==
1430 statusT::success );
1431 REQUIRE( workspace.baseRank() == 3 );
1432 REQUIRE( workspace.maximumDeleted() == 4 );
1433 REQUIRE( workspace.backend() == backendT::leadingCovariance );
1434 REQUIRE( workspace.prepared() );
1435 REQUIRE( result.baseRank() == 3 );
1436 REQUIRE( result.outputRank() == 3 );
1437 REQUIRE( result.lapackInfo() == 0 );
1438 REQUIRE( result.minimumPSDValue() >= -1e-4f );
1439
1440 const floatVectorT leadingSquared = result.squaredSingularValues();
1441 REQUIRE( workspace.prepare( 3, 4, backendT::stableCore ) == statusT::success );
1443 mx::math::svdDeletionCore( result, singular, deleted, 3, workspace, backendT::stableCore ) ) );
1444 REQUIRE( ( result.squaredSingularValues() - leadingSquared ).matrix().norm() < 2e-4f );
1445 floatMatrixT floatIdentity( 3, 3 );
1446 floatIdentity.matrix().setIdentity();
1447 REQUIRE( mx::math::validateSvdDeletionFactor( floatIdentity ) == statusT::success );
1448
1449 mx::math::svdDeletionWorkspace<float> movedWorkspace( std::move( workspace ) );
1450 REQUIRE( !workspace.prepared() );
1451 REQUIRE( movedWorkspace.prepared() );
1452 REQUIRE( movedWorkspace.maximumDeleted() == 4 );
1453 mx::math::svdDeletionWorkspace<float> assignedWorkspace;
1454 assignedWorkspace = std::move( movedWorkspace );
1455 REQUIRE( !movedWorkspace.prepared() );
1456 REQUIRE( movedWorkspace.baseRank() == 0 );
1457 REQUIRE( movedWorkspace.maximumDeleted() == 0 );
1458 REQUIRE( movedWorkspace.backend() == backendT::stableCore );
1459 REQUIRE( movedWorkspace.lapackInfo() == 0 );
1460 movedWorkspace.clear();
1461 REQUIRE( !movedWorkspace.prepared() );
1462 REQUIRE( assignedWorkspace.prepared() );
1464 mx::math::svdDeletionCore( result, singular, deleted, 3, assignedWorkspace, backendT::stableCore ) ) );
1465 REQUIRE( movedWorkspace.prepare( 3, 1, backendT::leadingCovariance ) == statusT::success );
1466 REQUIRE( movedWorkspace.prepared() );
1467 movedWorkspace.clear();
1468
1469 const floatMatrixT physicalLeft = sineFactor( 6, 3 ).cast<float>();
1470 const floatMatrixT physicalRight = sineFactor( 5, 3 ).cast<float>();
1471 floatVectorT physicalSingular( 3 );
1472 physicalSingular << 7, 3, 0.5f;
1473 const Eigen::MatrixXf physicalMatrix =
1474 physicalLeft.matrix() * physicalSingular.matrix().asDiagonal() * physicalRight.matrix().transpose();
1475 Eigen::MatrixXf retainedMatrix( 4, 5 );
1476 retainedMatrix.row( 0 ) = physicalMatrix.row( 1 );
1477 retainedMatrix.row( 1 ) = physicalMatrix.row( 2 );
1478 retainedMatrix.row( 2 ) = physicalMatrix.row( 3 );
1479 retainedMatrix.row( 3 ) = physicalMatrix.row( 5 );
1480 Eigen::JacobiSVD<Eigen::MatrixXf> directFloat( retainedMatrix, Eigen::ComputeThinV );
1481 const std::vector<Eigen::Index> physicalDeleted{ 0, 4 };
1482 for( const backendT backend : { backendT::leadingCovariance, backendT::stableCore } )
1483 {
1485 mx::math::svdDeletionWorkspace<float> physicalWorkspace;
1487 physicalSingular,
1488 physicalLeft,
1489 physicalDeleted,
1490 3,
1491 physicalWorkspace,
1492 backend ) ) );
1493 REQUIRE( ( physicalResult.singularValues().matrix() - directFloat.singularValues().head( 3 ) ).norm() < 2e-4f );
1494 }
1495
1496 mx::math::svdDeletionResult<float> movedResult( std::move( result ) );
1497 REQUIRE( movedResult.baseRank() == 3 );
1499 assignedResult = std::move( movedResult );
1500 REQUIRE( assignedResult.baseRank() == 3 );
1501 REQUIRE( movedResult.baseRank() == 0 );
1502 REQUIRE( movedResult.outputRank() == 0 );
1503 REQUIRE( movedResult.maximumOutputRank() == 0 );
1504 REQUIRE( movedResult.status() == statusT::notComputed );
1505 REQUIRE( movedResult.backend() == backendT::stableCore );
1506 REQUIRE( movedResult.clampedEigenvalues() == 0 );
1507 REQUIRE( movedResult.minimumPSDValue() == 0 );
1508 REQUIRE( movedResult.lapackInfo() == 0 );
1509 REQUIRE( movedResult.singularValues().size() == 0 );
1510 REQUIRE( movedResult.squaredSingularValues().size() == 0 );
1511 REQUIRE( movedResult.rotation().size() == 0 );
1512 REQUIRE( mx::math::svdDeletionCore( movedResult, singular, deleted, 0, assignedWorkspace, backendT::stableCore ) ==
1513 statusT::invalidInput );
1514 REQUIRE( movedResult.status() == statusT::invalidInput );
1515 REQUIRE( movedResult.prepare( 3, 2 ) == statusT::success );
1516 REQUIRE( movedResult.baseRank() == 3 );
1517 REQUIRE( movedResult.outputRank() == 2 );
1518
1519 assignedWorkspace.clear();
1520 REQUIRE( !assignedWorkspace.prepared() );
1521 REQUIRE( assignedWorkspace.baseRank() == 0 );
1522 REQUIRE( assignedWorkspace.maximumDeleted() == 0 );
1523 REQUIRE( assignedWorkspace.lapackInfo() == 0 );
1524}
1525
1526/// SVD deletion accepts views and reuses prepared capacity
1527/** Verifies non-owning Eigen views and capacity reuse without reallocation through svdDeletionCore and
1528 * validateSvdDeletionFactor.
1529 *
1530 * \ingroup svdDowndate_unit_tests
1531 */
1532TEST_CASE( "SVD deletion accepts views and reuses prepared capacity", "[math::svdDowndate][workspace][views]" )
1533{
1534 hookGuard guard;
1535 vectorT singularStorage( 4 );
1536 singularStorage << 8, 4, 2, 0.5;
1537 matrixT factorStorage( 6, 4 );
1538 factorStorage.setZero();
1539 factorStorage.matrix().leftCols( 3 ) = sineFactor( 6, 3 ).matrix();
1540 matrixT deletedStorage( 3, 4 );
1541 deletedStorage.setZero();
1542 deletedStorage.matrix().topLeftCorner( 1, 3 ) = factorStorage.matrix().block( 2, 0, 1, 3 );
1543
1544 REQUIRE( mx::math::validateSvdDeletionFactor( factorStorage.leftCols( 3 ) ) == statusT::success );
1547 REQUIRE( result.prepare( 3, 3 ) == statusT::success );
1548 REQUIRE( workspace.prepare( 3, 3, backendT::stableCore ) == statusT::success );
1549
1550 failingOperation = mx::math::detail::svdDeletionTestOperation::prepareWorkspace;
1551 mx::math::detail::svdDeletionHooks<double>().operation = throwAllocation;
1553 singularStorage.head( 3 ),
1554 deletedStorage.topLeftCorner( 1, 3 ),
1555 2,
1556 workspace,
1557 backendT::stableCore ) ) );
1558 REQUIRE( workspace.maximumDeleted() == 3 );
1559 REQUIRE( result.maximumOutputRank() == 3 );
1560 REQUIRE( result.outputRank() == 2 );
1561 REQUIRE( result.rotation().cols() == 2 );
1562 REQUIRE( result.singularValues().size() == 3 );
1563
1564 matrixT tooManyDeleted( 4, 3 );
1565 tooManyDeleted.setZero();
1566 REQUIRE( mx::math::svdDeletionCore( result,
1567 singularStorage.head( 3 ),
1568 tooManyDeleted,
1569 3,
1570 workspace,
1571 backendT::stableCore ) == statusT::allocationFailure );
1572}
1573
1574/// SVD deletion accepts under-aligned consumer storage
1575/** Verifies that validateSvdDeletionFactor and svdRemoveRows use unaligned raw views at the shared-library boundary
1576 * when the consumer's Eigen packet-alignment setting is smaller than mxlib's setting.
1577 *
1578 * \ingroup svdDowndate_unit_tests
1579 */
1580TEST_CASE( "SVD deletion accepts under-aligned consumer storage", "[math::svdDowndate][abi][alignment]" )
1581{
1582#ifdef MXLIB_SVD_DELETION_TEST_CONSUMER_ALIGNMENT
1583 STATIC_REQUIRE( EIGEN_MAX_ALIGN_BYTES == 16 );
1584 STATIC_REQUIRE( EIGEN_MAX_STATIC_ALIGN_BYTES == 16 );
1585#endif
1586
1587 constexpr Eigen::Index rows{ 7 };
1588 constexpr Eigen::Index rank{ 3 };
1589 constexpr std::uintptr_t testedAlignment{ 32 };
1590
1591 std::vector<realT> factorStorage( static_cast<std::size_t>( rows * rank ) + 4 );
1592 realT *factorData = factorStorage.data();
1593 while( reinterpret_cast<std::uintptr_t>( factorData ) % testedAlignment != 16 )
1594 {
1595 ++factorData;
1596 }
1597 REQUIRE( reinterpret_cast<std::uintptr_t>( factorData ) % testedAlignment == 16 );
1598
1599 using unalignedMatrixMap = Eigen::Map<matrixT, Eigen::Unaligned>;
1600 unalignedMatrixMap factor( factorData, rows, rank );
1601 factor = sineFactor( rows, rank );
1602 REQUIRE( mx::math::validateSvdDeletionFactor( factor ) == statusT::success );
1603
1604 std::vector<realT> singularStorage( static_cast<std::size_t>( rank ) + 4 );
1605 realT *singularData = singularStorage.data();
1606 while( reinterpret_cast<std::uintptr_t>( singularData ) % testedAlignment != 16 )
1607 {
1608 ++singularData;
1609 }
1610 REQUIRE( reinterpret_cast<std::uintptr_t>( singularData ) % testedAlignment == 16 );
1611
1612 using unalignedVectorMap = Eigen::Map<vectorT, Eigen::Unaligned>;
1613 unalignedVectorMap singularValues( singularData, rank );
1614 singularValues << 5, 3, 1;
1615
1616 const std::vector<Eigen::Index> deletedRows{ 2 };
1619 REQUIRE(
1620 mx::math::svdRemoveRows( result, singularValues, factor, deletedRows, rank, workspace, backendT::stableCore ) ==
1621 statusT::success );
1622 REQUIRE( result.rotation().rows() == rank );
1623 REQUIRE( result.rotation().cols() == rank );
1624 REQUIRE( result.rotation().matrix().squaredNorm() == Approx( static_cast<realT>( rank ) ).margin( 1e-12 ) );
1625 REQUIRE( result.singularValues().matrix().squaredNorm() > 0 );
1626}
1627
1628/// SVD deletion accepts a default empty ABI index descriptor
1629/** Verifies that the raw svdRemoveRowsAbiV2 entry point treats a default-constructed empty index descriptor as an
1630 * identity deletion.
1631 *
1632 * \ingroup svdDowndate_unit_tests
1633 */
1634TEST_CASE( "SVD deletion accepts a default empty ABI index descriptor", "[math::svdDowndate][abi][identity]" )
1635{
1636 vectorT singular( 2 );
1637 singular << 4, 1;
1638 matrixT factor = identityMatrix( 2 );
1641
1642 const mx::math::svdDeletionConstVectorViewV2<realT> singularView{ singular.data(), singular.size() };
1643 const mx::math::svdDeletionConstMatrixViewV2<realT> factorView{ factor.data(),
1644 factor.rows(),
1645 factor.cols(),
1646 factor.outerStride() };
1647 const mx::math::svdDeletionConstIndexViewV2 noDeletedIndices;
1648 REQUIRE( mx::math::detail::svdRemoveRowsAbiV2<realT>( result,
1649 singularView,
1650 factorView,
1651 noDeletedIndices,
1652 2,
1653 workspace,
1654 backendT::stableCore ) == statusT::success );
1655 REQUIRE( ( result.singularValues() - singular ).matrix().norm() == Approx( 0.0 ) );
1656 REQUIRE( ( result.rotation() - identityMatrix( 2 ) ).matrix().norm() == Approx( 0.0 ) );
1657}
1658
1659/// SVD deletion rejects malformed ABI descriptors
1660/** Verifies the ABI-v2 raw entry points reject vector, matrix, and index descriptors whose claimed storage cannot be
1661 * addressed safely.
1662 *
1663 * \ingroup svdDowndate_unit_tests
1664 */
1665TEST_CASE( "SVD deletion rejects malformed ABI descriptors", "[math::svdDowndate][abi][errors]" )
1666{
1667 vectorT singular( 2 );
1668 singular << 4, 1;
1669 matrixT factor = identityMatrix( 2 );
1670 const Eigen::Index deletedIndex{ 0 };
1673
1674 const mx::math::svdDeletionConstVectorViewV2<realT> singularView{ singular.data(), singular.size() };
1675 const mx::math::svdDeletionConstMatrixViewV2<realT> factorView{ factor.data(),
1676 factor.rows(),
1677 factor.cols(),
1678 factor.outerStride() };
1679 const auto *misalignedScalar =
1680 reinterpret_cast<const realT *>( reinterpret_cast<const unsigned char *>( singular.data() ) + 1 );
1681
1682 REQUIRE( result.prepare( -1, 1 ) == statusT::invalidInput );
1683 REQUIRE( mx::math::detail::validateSvdDeletionFactorAbiV2( { factor.data(), -1, 2, factor.outerStride() }, 0 ) ==
1684 statusT::invalidInput );
1685 REQUIRE( mx::math::detail::validateSvdDeletionFactorAbiV2(
1687 0.0 ) == statusT::invalidInput );
1688 REQUIRE( mx::math::detail::validateSvdDeletionFactorAbiV2( { misalignedScalar, 2, 2, 2 }, 0.0 ) ==
1689 statusT::invalidInput );
1690 REQUIRE( mx::math::detail::validateSvdDeletionFactorAbiV2( { factor.data(), 2, 2, 1 }, 0 ) ==
1691 statusT::invalidInput );
1692 REQUIRE(
1693 mx::math::detail::validateSvdDeletionFactorAbiV2(
1694 { factor.data(), std::numeric_limits<std::int64_t>::max(), 1, std::numeric_limits<std::int64_t>::max() },
1695 0 ) == statusT::invalidInput );
1696 REQUIRE( mx::math::detail::validateSvdDeletionFactorAbiV2(
1697 { factor.data(), 2, 2, std::numeric_limits<std::int64_t>::max() },
1698 0 ) == statusT::invalidInput );
1699 REQUIRE( mx::math::detail::validateSvdDeletionFactorAbiV2(
1701 0.0F ) == statusT::invalidInput );
1702 REQUIRE( mx::math::detail::svdDeletionLeadingCoreAbiV2<realT>(
1703 result,
1704 { singular.data(), std::numeric_limits<std::int64_t>::max() },
1705 factorView,
1706 2,
1707 workspace ) == statusT::invalidInput );
1708 REQUIRE( mx::math::detail::svdDeletionLeadingCoreAbiV2<realT>( result,
1709 { misalignedScalar, singular.size() },
1710 factorView,
1711 2,
1712 workspace ) == statusT::invalidInput );
1713 REQUIRE( mx::math::detail::svdDeletionStableCoreAbiV2<realT>(
1714 result,
1715 { singular.data(), std::numeric_limits<std::int64_t>::max() },
1716 factorView,
1717 2,
1718 workspace ) == statusT::invalidInput );
1719 REQUIRE( mx::math::detail::svdRemoveRowsAbiV2<realT>(
1720 result,
1721 singularView,
1722 factorView,
1723 { &deletedIndex, std::numeric_limits<std::int64_t>::max(), sizeof( deletedIndex ) },
1724 2,
1725 workspace,
1726 backendT::stableCore ) == statusT::invalidInput );
1727 REQUIRE( mx::math::detail::svdRemoveRowsAbiV2<realT>( result,
1728 singularView,
1729 factorView,
1730 { &deletedIndex, 1, 3 },
1731 2,
1732 workspace,
1733 backendT::stableCore ) == statusT::invalidInput );
1734 REQUIRE( mx::math::detail::svdRemoveRowsAbiV2<realT>( result,
1735 singularView,
1736 factorView,
1737 { nullptr, 1, sizeof( deletedIndex ) },
1738 2,
1739 workspace,
1740 backendT::stableCore ) == statusT::invalidInput );
1741 REQUIRE( mx::math::detail::svdRemoveColumnsAbiV2<realT>( result,
1742 singularView,
1743 factorView,
1744 { nullptr, 1, sizeof( deletedIndex ) },
1745 2,
1746 workspace,
1747 backendT::stableCore ) == statusT::invalidInput );
1748
1749 const std::int8_t deleted8{ 0 };
1750 const std::int16_t deleted16{ 0 };
1751 const std::int32_t deleted32{ 0 };
1752 for( const mx::math::svdDeletionConstIndexViewV2 indices :
1753 { mx::math::svdDeletionConstIndexViewV2{ &deleted8, 1, sizeof( deleted8 ) },
1754 mx::math::svdDeletionConstIndexViewV2{ &deleted16, 1, sizeof( deleted16 ) },
1755 mx::math::svdDeletionConstIndexViewV2{ &deleted32, 1, sizeof( deleted32 ) } } )
1756 {
1758 mx::math::detail::svdRemoveRowsAbiV2<
1759 realT>( result, singularView, factorView, indices, 2, workspace, backendT::leadingCovariance ) ) );
1760 }
1761}
1762
1763/// SVD deletion reports invalid inputs and allocation failures
1764/** Verifies public validation, status text, allocation failures, and complete-side rejection in the SVD
1765 * deletion system.
1766 *
1767 * \ingroup svdDowndate_unit_tests
1768 */
1769TEST_CASE( "SVD deletion reports invalid inputs and allocation failures", "[math::svdDowndate][errors]" )
1770{
1771 hookGuard guard;
1772 REQUIRE( std::string( mx::math::svdDeletionBackendName( backendT::leadingCovariance ) ) == "leadingCovariance" );
1773 REQUIRE( std::string( mx::math::svdDeletionBackendName( backendT::stableCore ) ) == "stableCore" );
1774 REQUIRE( std::string( mx::math::svdDeletionBackendName( static_cast<backendT>( 99 ) ) ) == "unknown" );
1775
1776 for( const statusT status : { statusT::notComputed,
1777 statusT::success,
1778 statusT::successWithClamping,
1779 statusT::invalidInput,
1780 statusT::allocationFailure,
1781 statusT::workspaceQueryFailure,
1782 statusT::solverFailure,
1783 statusT::nonFiniteOutput,
1784 statusT::invalidSolverOutput,
1785 statusT::rescalingOverflow,
1786 statusT::nonPositiveSemidefinite,
1787 statusT::factorNotOrthonormal } )
1788 {
1789 REQUIRE( std::string( mx::math::svdDeletionStatusName( status ) ) != "unknown" );
1790 }
1791 REQUIRE( std::string( mx::math::svdDeletionStatusName( static_cast<statusT>( 99 ) ) ) == "unknown" );
1792 REQUIRE( mx::math::svdDeletionSucceeded( statusT::success ) );
1793 REQUIRE( mx::math::svdDeletionSucceeded( statusT::successWithClamping ) );
1794 REQUIRE( !mx::math::svdDeletionSucceeded( statusT::solverFailure ) );
1795
1796 vectorT singular( 2 );
1797 singular << 4, 1;
1798 matrixT factor = identityMatrix( 2 );
1801
1802 mx::math::svdDeletionResult<realT> movedFromResult;
1803 mx::math::svdDeletionResult<realT> resultOwner( std::move( movedFromResult ) );
1804 failingOperation = mx::math::detail::svdDeletionTestOperation::prepareResult;
1805 mx::math::detail::svdDeletionHooks<double>().operation = throwAllocation;
1806 REQUIRE( mx::math::svdDeletionCore( movedFromResult, singular, factor, 0, workspace, backendT::stableCore ) ==
1807 statusT::allocationFailure );
1808 mx::math::detail::svdDeletionHooks<double>().operation = throwLengthError;
1809 REQUIRE( movedFromResult.prepare( 2, 2 ) == statusT::allocationFailure );
1810 mx::math::detail::svdDeletionHooks<double>().operation = nullptr;
1811 REQUIRE( movedFromResult.prepare( 2, 2 ) == statusT::success );
1812
1813 mx::math::svdDeletionWorkspace<realT> movedFromWorkspace;
1814 mx::math::svdDeletionWorkspace<realT> workspaceOwner( std::move( movedFromWorkspace ) );
1815 failingOperation = mx::math::detail::svdDeletionTestOperation::prepareWorkspace;
1816 mx::math::detail::svdDeletionHooks<double>().operation = throwAllocation;
1817 REQUIRE( movedFromWorkspace.prepare( 2, 1, backendT::stableCore ) == statusT::allocationFailure );
1818 mx::math::detail::svdDeletionHooks<double>().operation = throwLengthError;
1819 REQUIRE( movedFromWorkspace.prepare( 2, 1, backendT::stableCore ) == statusT::allocationFailure );
1820 mx::math::detail::svdDeletionHooks<double>().operation = nullptr;
1821 REQUIRE( movedFromWorkspace.prepare( 2, 1, backendT::stableCore ) == statusT::success );
1822
1823 REQUIRE( result.status() == statusT::notComputed );
1824 REQUIRE( result.prepare( 0, 0 ) == statusT::invalidInput );
1825 REQUIRE( result.prepare( 2, 3 ) == statusT::invalidInput );
1826 REQUIRE( workspace.prepare( 0, -1, backendT::stableCore ) == statusT::invalidInput );
1827 REQUIRE( workspace.prepare( 2, 1, static_cast<backendT>( 99 ) ) == statusT::invalidInput );
1828
1829 failingOperation = mx::math::detail::svdDeletionTestOperation::prepareResult;
1830 mx::math::detail::svdDeletionHooks<double>().operation = throwAllocation;
1831 REQUIRE( result.prepare( 2, 2 ) == statusT::allocationFailure );
1832
1833 failingOperation = mx::math::detail::svdDeletionTestOperation::prepareWorkspace;
1834 REQUIRE( workspace.prepare( 2, 1, backendT::stableCore ) == statusT::allocationFailure );
1835
1836 failingOperation = mx::math::detail::svdDeletionTestOperation::validateFactor;
1837 REQUIRE( mx::math::validateSvdDeletionFactor( factor ) == statusT::allocationFailure );
1838 mx::math::detail::svdDeletionHooks<double>().operation = nullptr;
1839
1840 failingOperation = mx::math::detail::svdDeletionTestOperation::prepareResult;
1841 mx::math::detail::svdDeletionHooks<double>().operation = throwLengthError;
1842 REQUIRE( result.prepare( 3, 3 ) == statusT::allocationFailure );
1843 failingOperation = mx::math::detail::svdDeletionTestOperation::prepareWorkspace;
1844 REQUIRE( workspace.prepare( 3, 1, backendT::stableCore ) == statusT::allocationFailure );
1845 failingOperation = mx::math::detail::svdDeletionTestOperation::validateFactor;
1846 REQUIRE( mx::math::validateSvdDeletionFactor( factor ) == statusT::allocationFailure );
1847 mx::math::detail::svdDeletionHooks<double>().operation = nullptr;
1848
1849 matrixT noDeletedRows( 0, 2 );
1850 failingOperation = mx::math::detail::svdDeletionTestOperation::prepareResult;
1851 mx::math::detail::svdDeletionHooks<double>().operation = throwAllocation;
1852 mx::math::svdDeletionResult<realT> identityFailure;
1853 REQUIRE( mx::math::svdDeletionLeadingCore( identityFailure, singular, noDeletedRows, 2, workspace ) ==
1854 statusT::allocationFailure );
1855
1856 matrixT oneDeletedRow( 1, 2 );
1857 oneDeletedRow << 0.25, 0.5;
1858 mx::math::svdDeletionResult<realT> coreResultFailure;
1859 REQUIRE( mx::math::svdDeletionLeadingCore( coreResultFailure, singular, oneDeletedRow, 2, workspace ) ==
1860 statusT::allocationFailure );
1861
1862 mx::math::svdDeletionResult<realT> removeResultFailure;
1863 const std::vector<Eigen::Index> oneRow{ 0 };
1864 REQUIRE( mx::math::svdRemoveRows( removeResultFailure,
1865 singular,
1866 factor,
1867 oneRow,
1868 2,
1869 workspace,
1870 backendT::leadingCovariance ) == statusT::allocationFailure );
1871 mx::math::detail::svdDeletionHooks<double>().operation = nullptr;
1872
1873 REQUIRE( workspace.prepare( 2, 1, backendT::stableCore ) == statusT::success );
1874 REQUIRE( workspace.prepare( 2, 1, backendT::stableCore ) == statusT::success );
1875
1876 const std::vector<Eigen::Index> allRows{ 0, 1 };
1877 REQUIRE( mx::math::svdRemoveRows( result, singular, factor, allRows, 2, workspace, backendT::stableCore ) ==
1878 statusT::invalidInput );
1879 const std::vector<Eigen::Index> duplicate{ 0, 0 };
1880 REQUIRE( mx::math::svdRemoveRows( result, singular, factor, duplicate, 2, workspace, backendT::stableCore ) ==
1881 statusT::invalidInput );
1882 const std::vector<Eigen::Index> unsorted{ 1, 0 };
1883 REQUIRE( mx::math::svdRemoveRows( result, singular, factor, unsorted, 2, workspace, backendT::stableCore ) ==
1884 statusT::invalidInput );
1885 const std::vector<Eigen::Index> negative{ -1 };
1886 REQUIRE( mx::math::svdRemoveRows( result, singular, factor, negative, 2, workspace, backendT::stableCore ) ==
1887 statusT::invalidInput );
1888 const std::vector<Eigen::Index> outOfRange{ 2 };
1889 REQUIRE( mx::math::svdRemoveColumns( result, singular, factor, outOfRange, 2, workspace, backendT::stableCore ) ==
1890 statusT::invalidInput );
1891
1892 matrixT nonfinite = factor;
1893 nonfinite( 0, 0 ) = std::numeric_limits<realT>::infinity();
1894 REQUIRE( mx::math::validateSvdDeletionFactor( nonfinite ) == statusT::invalidInput );
1895 REQUIRE( mx::math::validateSvdDeletionFactor( factor, realT( -1 ) ) == statusT::invalidInput );
1896
1897 matrixT tooWide( 1, 2 );
1898 tooWide.setZero();
1899 REQUIRE( mx::math::validateSvdDeletionFactor( tooWide ) == statusT::invalidInput );
1900 const std::vector<Eigen::Index> noIndices;
1901 REQUIRE( mx::math::svdRemoveRows( result, singular, tooWide, noIndices, 2, workspace, backendT::stableCore ) ==
1902 statusT::invalidInput );
1903 matrixT emptyFactor( 2, 0 );
1904 REQUIRE( mx::math::validateSvdDeletionFactor( emptyFactor ) == statusT::invalidInput );
1905 matrixT overflowFactor( 2, 1 );
1906 overflowFactor.setConstant( std::numeric_limits<realT>::max() );
1907 REQUIRE( mx::math::validateSvdDeletionFactor( overflowFactor ) == statusT::invalidInput );
1908
1909 vectorT invalidSingular = singular;
1910 invalidSingular( 1 ) = -1;
1911 const std::vector<Eigen::Index> one{ 0 };
1912 REQUIRE( mx::math::svdRemoveRows( result, invalidSingular, factor, one, 2, workspace, backendT::stableCore ) ==
1913 statusT::invalidInput );
1914 invalidSingular << 1, 2;
1915 REQUIRE( mx::math::svdRemoveRows( result, invalidSingular, factor, one, 2, workspace, backendT::stableCore ) ==
1916 statusT::invalidInput );
1917 invalidSingular = singular;
1918 invalidSingular( 0 ) = std::numeric_limits<realT>::infinity();
1919 REQUIRE( mx::math::svdRemoveRows( result, invalidSingular, factor, one, 2, workspace, backendT::stableCore ) ==
1920 statusT::invalidInput );
1921 REQUIRE( mx::math::svdRemoveRows( result, singular, factor, one, 0, workspace, backendT::stableCore ) ==
1922 statusT::invalidInput );
1923
1924 matrixT wrongColumns( 0, 1 );
1925 REQUIRE( mx::math::svdDeletionCore( result, singular, wrongColumns, 2, workspace, backendT::stableCore ) ==
1926 statusT::invalidInput );
1927 REQUIRE( mx::math::svdDeletionLeadingCore( result, singular, wrongColumns, 2, workspace ) ==
1928 statusT::invalidInput );
1929 matrixT nonfiniteDeleted( 1, 2 );
1930 nonfiniteDeleted << 0, std::numeric_limits<realT>::infinity();
1931 REQUIRE( mx::math::svdDeletionCore( result, singular, nonfiniteDeleted, 2, workspace, backendT::stableCore ) ==
1932 statusT::invalidInput );
1933 REQUIRE(
1934 mx::math::svdDeletionCore( result, singular, factor.topRows( 1 ), 2, workspace, static_cast<backendT>( 99 ) ) ==
1935 statusT::invalidInput );
1936
1937 matrixT selectedFinite = factor;
1938 selectedFinite( 1, 0 ) = std::numeric_limits<realT>::infinity();
1940 mx::math::svdRemoveRows( result, singular, selectedFinite, one, 2, workspace, backendT::leadingCovariance ) ) );
1941 const std::vector<Eigen::Index> selectNonfinite{ 1 };
1942 REQUIRE( mx::math::svdRemoveRows( result,
1943 singular,
1944 selectedFinite,
1945 selectNonfinite,
1946 2,
1947 workspace,
1948 backendT::leadingCovariance ) == statusT::invalidInput );
1949
1950 const Eigen::Index huge = static_cast<Eigen::Index>( std::numeric_limits<MXLAPACK_INT>::max() );
1951 REQUIRE( workspace.prepare( huge, 0, backendT::stableCore ) == statusT::invalidInput );
1952 REQUIRE( workspace.prepare( huge, 1, backendT::stableCore ) == statusT::invalidInput );
1953}
1954
1955/// SVD deletion reports workspace query failures
1956/** Verifies SYEVR and GESVD workspace-query failure reporting through svdDeletionWorkspace::prepare and
1957 * svdDeletionCore.
1958 *
1959 * \ingroup svdDowndate_unit_tests
1960 */
1961TEST_CASE( "SVD deletion reports workspace query failures", "[math::svdDowndate][errors][query]" )
1962{
1963 hookGuard guard;
1964
1965 SECTION( "SYEVR query INFO" )
1966 {
1968 syevrMode = solverHookMode::queryFailure;
1969 mx::math::detail::svdDeletionHooks<double>().syevr = syevrHook;
1970 REQUIRE( workspace.prepare( 3, 1, backendT::leadingCovariance ) == statusT::workspaceQueryFailure );
1971 REQUIRE( workspace.lapackInfo() == 61 );
1972 REQUIRE( !workspace.prepared() );
1973 }
1974
1975 SECTION( "SYEVR floating query size" )
1976 {
1978 syevrMode = solverHookMode::invalidQuery;
1979 mx::math::detail::svdDeletionHooks<double>().syevr = syevrHook;
1980 REQUIRE( workspace.prepare( 3, 1, backendT::leadingCovariance ) == statusT::workspaceQueryFailure );
1981 REQUIRE( workspace.lapackInfo() == 0 );
1982 }
1983
1984 SECTION( "SYEVR integer query size" )
1985 {
1987 syevrMode = solverHookMode::invalidIntegerQuery;
1988 mx::math::detail::svdDeletionHooks<double>().syevr = syevrHook;
1989 REQUIRE( workspace.prepare( 3, 1, backendT::stableCore ) == statusT::workspaceQueryFailure );
1990 }
1991
1992 SECTION( "GESVD query INFO" )
1993 {
1995 gesvdMode = solverHookMode::queryFailure;
1996 mx::math::detail::svdDeletionHooks<double>().gesvd = gesvdHook;
1997 REQUIRE( workspace.prepare( 3, 1, backendT::stableCore ) == statusT::workspaceQueryFailure );
1998 REQUIRE( workspace.lapackInfo() == 62 );
1999 }
2000
2001 SECTION( "GESVD query size" )
2002 {
2004 gesvdMode = solverHookMode::invalidQuery;
2005 mx::math::detail::svdDeletionHooks<double>().gesvd = gesvdHook;
2006 REQUIRE( workspace.prepare( 3, 1, backendT::stableCore ) == statusT::workspaceQueryFailure );
2007 REQUIRE( workspace.lapackInfo() == 0 );
2008 }
2009
2010 SECTION( "query failure propagated to a result" )
2011 {
2012 vectorT singular( 2 );
2013 singular << 4, 1;
2014 matrixT deleted( 1, 2 );
2015 deleted << 0.25, 0.5;
2018 syevrMode = solverHookMode::queryFailure;
2019 mx::math::detail::svdDeletionHooks<double>().syevr = syevrHook;
2020 REQUIRE( mx::math::svdDeletionCore( result, singular, deleted, 2, workspace, backendT::leadingCovariance ) ==
2021 statusT::workspaceQueryFailure );
2022 REQUIRE( result.status() == statusT::workspaceQueryFailure );
2023 REQUIRE( result.lapackInfo() == 61 );
2024 REQUIRE( result.minimumPSDValue() == Approx( 0.0 ) );
2025 }
2026}
2027
2028/// SVD deletion reports numerical solver outcomes
2029/** Verifies solve failures, malformed solver output, PSD clamping, indefiniteness, and stable rescaling through
2030 * svdDeletionLeadingCore and svdDeletionStableCore.
2031 *
2032 * \ingroup svdDowndate_unit_tests
2033 */
2034TEST_CASE( "SVD deletion reports numerical solver outcomes", "[math::svdDowndate][errors][solver]" )
2035{
2036 hookGuard guard;
2037 vectorT singular( 3 );
2038 singular << 8, 4, 2;
2039 matrixT oneDeleted( 1, 3 );
2040 oneDeleted << 0.25, 0.2, 0.1;
2041 matrixT twoDeleted( 2, 3 );
2042 twoDeleted << 0.25, 0.2, 0.1, 0.1, 0.15, 0.2;
2043
2044 SECTION( "leading SYEVR solve failure" )
2045 {
2048 REQUIRE( workspace.prepare( 3, 1, backendT::leadingCovariance ) == statusT::success );
2049 syevrMode = solverHookMode::solveFailure;
2050 mx::math::detail::svdDeletionHooks<double>().syevr = syevrHook;
2051 REQUIRE( mx::math::svdDeletionLeadingCore( result, singular, oneDeleted, 3, workspace ) ==
2052 statusT::solverFailure );
2053 REQUIRE( result.lapackInfo() == 71 );
2054 }
2055
2056 SECTION( "leading SYEVR count mismatch" )
2057 {
2060 REQUIRE( workspace.prepare( 3, 1, backendT::leadingCovariance ) == statusT::success );
2061 syevrMode = solverHookMode::countMismatch;
2062 mx::math::detail::svdDeletionHooks<double>().syevr = syevrHook;
2063 REQUIRE( mx::math::svdDeletionLeadingCore( result, singular, oneDeleted, 3, workspace ) ==
2064 statusT::solverFailure );
2065 REQUIRE( result.lapackInfo() == 0 );
2066 }
2067
2068 SECTION( "leading non-finite values and vectors" )
2069 {
2070 for( const solverHookMode mode : { solverHookMode::nonFiniteValue, solverHookMode::nonFiniteVector } )
2071 {
2074 REQUIRE( workspace.prepare( 3, 1, backendT::leadingCovariance ) == statusT::success );
2075 syevrMode = mode;
2076 mx::math::detail::svdDeletionHooks<double>().syevr = syevrHook;
2077 REQUIRE( mx::math::svdDeletionLeadingCore( result, singular, oneDeleted, 3, workspace ) ==
2078 statusT::nonFiniteOutput );
2079 }
2080 }
2081
2082 SECTION( "leading invalid ordering" )
2083 {
2086 REQUIRE( workspace.prepare( 3, 1, backendT::leadingCovariance ) == statusT::success );
2087 syevrMode = solverHookMode::invalidOrdering;
2088 mx::math::detail::svdDeletionHooks<double>().syevr = syevrHook;
2089 REQUIRE( mx::math::svdDeletionLeadingCore( result, singular, oneDeleted, 3, workspace ) ==
2090 statusT::invalidSolverOutput );
2091 }
2092
2093 SECTION( "leading clamping and indefiniteness" )
2094 {
2097 REQUIRE( workspace.prepare( 3, 1, backendT::leadingCovariance ) == statusT::success );
2098 mx::math::detail::svdDeletionHooks<double>().syevr = syevrHook;
2099 syevrMode = solverHookMode::roundoffClamp;
2100 REQUIRE( mx::math::svdDeletionLeadingCore( result, singular, oneDeleted, 3, workspace ) ==
2101 statusT::successWithClamping );
2102 REQUIRE( result.clampedEigenvalues() == 1 );
2103 REQUIRE( result.minimumPSDValue() == Approx( -std::numeric_limits<realT>::epsilon() ) );
2104
2105 syevrMode = solverHookMode::indefinite;
2106 REQUIRE( mx::math::svdDeletionLeadingCore( result, singular, oneDeleted, 3, workspace ) ==
2107 statusT::nonPositiveSemidefinite );
2108 REQUIRE( result.minimumPSDValue() == Approx( -0.25 ) );
2109 }
2110
2111 SECTION( "stable complement solver outcomes" )
2112 {
2115 REQUIRE( workspace.prepare( 3, 2, backendT::stableCore ) == statusT::success );
2116 mx::math::detail::svdDeletionHooks<double>().syevr = syevrHook;
2117
2118 for( const solverHookMode mode : { solverHookMode::nonFiniteValue, solverHookMode::nonFiniteVector } )
2119 {
2120 syevrMode = mode;
2121 REQUIRE( mx::math::svdDeletionStableCore( result, singular, oneDeleted, 3, workspace ) ==
2122 statusT::nonFiniteOutput );
2123 }
2124 syevrMode = solverHookMode::countMismatch;
2125 REQUIRE( mx::math::svdDeletionStableCore( result, singular, oneDeleted, 3, workspace ) ==
2126 statusT::solverFailure );
2127 syevrMode = solverHookMode::invalidOrdering;
2128 REQUIRE( mx::math::svdDeletionStableCore( result, singular, twoDeleted, 3, workspace ) ==
2129 statusT::invalidSolverOutput );
2130 syevrMode = solverHookMode::indefinite;
2131 REQUIRE( mx::math::svdDeletionStableCore( result, singular, oneDeleted, 3, workspace ) ==
2132 statusT::nonPositiveSemidefinite );
2133 REQUIRE( result.minimumPSDValue() == Approx( -0.25 ) );
2134 syevrMode = solverHookMode::roundoffClamp;
2135 REQUIRE( mx::math::svdDeletionStableCore( result, singular, oneDeleted, 3, workspace ) ==
2136 statusT::successWithClamping );
2137 REQUIRE( result.clampedEigenvalues() == 1 );
2138 }
2139
2140 SECTION( "stable GESVD solver outcomes" )
2141 {
2142 for( const auto [mode, expected] : std::vector<std::pair<solverHookMode, statusT>>{
2143 { solverHookMode::solveFailure, statusT::solverFailure },
2144 { solverHookMode::nonFiniteValue, statusT::nonFiniteOutput },
2145 { solverHookMode::nonFiniteVector, statusT::nonFiniteOutput },
2146 { solverHookMode::invalidOrdering, statusT::invalidSolverOutput },
2147 { solverHookMode::negativeSpectrum, statusT::invalidSolverOutput } } )
2148 {
2151 REQUIRE( workspace.prepare( 3, 1, backendT::stableCore ) == statusT::success );
2152 gesvdMode = mode;
2153 mx::math::detail::svdDeletionHooks<double>().gesvd = gesvdHook;
2154 REQUIRE( mx::math::svdDeletionStableCore( result, singular, oneDeleted, 3, workspace ) == expected );
2155 if( mode == solverHookMode::solveFailure )
2156 {
2157 REQUIRE( result.lapackInfo() == 72 );
2158 }
2159 mx::math::detail::svdDeletionHooks<double>().gesvd = nullptr;
2160 }
2161 }
2162
2163 SECTION( "stable rescaling retains a tiny normalized singular value" )
2164 {
2165 vectorT largeSingular( 1 );
2166 largeSingular << 1e200;
2167 matrixT deleted( 1, 1 );
2168 deleted << 0.5;
2171 REQUIRE( workspace.prepare( 1, 1, backendT::stableCore ) == statusT::success );
2172 gesvdMode = solverHookMode::tinySpectrum;
2173 mx::math::detail::svdDeletionHooks<double>().gesvd = gesvdHook;
2174 REQUIRE( mx::math::svdDeletionStableCore( result, largeSingular, deleted, 1, workspace ) == statusT::success );
2175 REQUIRE( result.singularValues()( 0 ) == Approx( 1.0 ).epsilon( 1e-12 ) );
2176 REQUIRE( result.squaredSingularValues()( 0 ) == Approx( 1.0 ).epsilon( 1e-12 ) );
2177 }
2178}
2179
2180/// SVD deletion reports rescaling overflow
2181/** Verifies explicit squared-result overflow reporting in unchanged, leading-core, and stable-core deletion
2182 * paths.
2183 *
2184 * \ingroup svdDowndate_unit_tests
2185 */
2186TEST_CASE( "SVD deletion reports rescaling overflow", "[math::svdDowndate][errors][overflow]" )
2187{
2188 vectorT singular( 1 );
2189 singular << std::numeric_limits<realT>::max() / 2;
2190 matrixT factor( 2, 1 );
2191 factor << std::sqrt( 0.5 ), std::sqrt( 0.5 );
2192 const std::vector<Eigen::Index> none;
2193 const std::vector<Eigen::Index> deletedIndex{ 0 };
2194
2195 for( const backendT backend : { backendT::leadingCovariance, backendT::stableCore } )
2196 {
2197 CAPTURE( mx::math::svdDeletionBackendName( backend ) );
2200 REQUIRE( mx::math::svdRemoveRows( result, singular, factor, none, 1, workspace, backend ) ==
2201 statusT::rescalingOverflow );
2202 const statusT deletionStatus =
2203 mx::math::svdRemoveRows( result, singular, factor, deletedIndex, 1, workspace, backend );
2204 CAPTURE( result.singularValues()( 0 ), result.squaredSingularValues()( 0 ) );
2205 REQUIRE( deletionStatus == statusT::rescalingOverflow );
2206 }
2207}
2208
2209} // namespace unitTest::math_svdDowndate_test
Result of deleting rows or columns from a represented thin SVD.
MXLIB_SVD_DELETION_HEADER_ADAPTER svdDeletionStatus prepare(Eigen::Index baseRank, Eigen::Index outputRank)
Prepare or reuse output storage for a base and requested active output rank.
MXLIB_SVD_DELETION_HEADER_ADAPTER svdDeletionConstMatrixRef< realT > rotation() const noexcept
Return the preserved-side rotation, with updated directions in columns.
svdDeletionStatus status() const noexcept
Return the most recent operation status.
MXLIB_SVD_DELETION_HEADER_ADAPTER svdDeletionConstVectorRef< realT > singularValues() const noexcept
Return an unaligned borrowed view of all baseRank() descending updated singular values.
realT minimumPSDValue() const noexcept
Return the smallest pre-clamp eigenvalue from the normalized backend PSD validation core.
std::int64_t clampedEigenvalues() const noexcept
Return the number of roundoff-scale negative eigenvalues clamped to zero.
std::int64_t outputRank() const noexcept
Return the requested published rank.
std::int64_t maximumOutputRank() const noexcept
Return the allocated rotation-column capacity for the current base rank.
MXLIB_SVD_DELETION_HEADER_ADAPTER svdDeletionConstVectorRef< realT > squaredSingularValues() const noexcept
Return an unaligned borrowed view of all corresponding descending squared singular values.
std::int64_t baseRank() const noexcept
Return the base factorization rank for which storage is prepared.
MXLAPACK_INT lapackInfo() const noexcept
Return the underlying LAPACK status from the most recent failed query or solve.
svdDeletionBackend backend() const noexcept
Return the backend that produced the current result.
Reusable, non-shared storage for SVD deletion operations.
bool prepared() const noexcept
Report whether this workspace has completed preparation.
MXLAPACK_INT lapackInfo() const noexcept
Return the underlying LAPACK status from the most recent failed workspace query.
void clear() noexcept
Release all prepared storage and reset dimensions.
std::int64_t baseRank() const noexcept
Return the prepared base rank.
svdDeletionBackend backend() const noexcept
Return the prepared numerical backend.
MXLIB_SVD_DELETION_HEADER_ADAPTER svdDeletionStatus prepare(Eigen::Index baseRank, Eigen::Index maximumDeleted, svdDeletionBackend backend)
Prepare reusable storage and LAPACK work arrays.
std::int64_t maximumDeleted() const noexcept
Return the prepared maximum deletion count.
constexpr T pi()
Get the value of pi.
Definition constants.hpp:62
TEST_CASE("SVD row deletion matches direct full SVDs", "[math::svdDowndate][rows]")
SVD row deletion matches direct full SVDs.
const char * svdDeletionStatusName(svdDeletionStatus status)
Return a stable text representation of an SVD deletion status.
MXLIB_SVD_DELETION_HEADER_ADAPTER svdDeletionStatus svdDeletionCore(svdDeletionResult< realT > &result, std::type_identity_t< svdDeletionConstVectorRef< realT > > singularValues, std::type_identity_t< svdDeletionConstMatrixRef< realT > > deletedRows, Eigen::Index outputRank, svdDeletionWorkspace< realT > &workspace, svdDeletionBackend backend=svdDeletionBackend::stableCore)
Delete supplied singular-factor rows with an explicitly selected backend.
MXLIB_SVD_DELETION_HEADER_ADAPTER svdDeletionStatus svdRemoveColumns(svdDeletionResult< realT > &result, std::type_identity_t< svdDeletionConstVectorRef< realT > > singularValues, std::type_identity_t< svdDeletionConstMatrixRef< realT > > rightFactor, std::span< const Eigen::Index > deletedIndices, Eigen::Index outputRank, svdDeletionWorkspace< realT > &workspace, svdDeletionBackend backend=svdDeletionBackend::stableCore)
Delete physical columns from the matrix represented by a thin SVD.
svdDeletionStatus
Completion status for an SVD deletion operation.
MXLIB_SVD_DELETION_HEADER_ADAPTER svdDeletionStatus validateSvdDeletionFactor(svdDeletionConstMatrixRef< float > factor, float tolerance=0)
Validate that a supplied thin singular-vector factor has orthonormal columns.
MXLIB_SVD_DELETION_HEADER_ADAPTER svdDeletionStatus svdDeletionLeadingCore(svdDeletionResult< realT > &result, std::type_identity_t< svdDeletionConstVectorRef< realT > > singularValues, std::type_identity_t< svdDeletionConstMatrixRef< realT > > deletedRows, Eigen::Index outputRank, svdDeletionWorkspace< realT > &workspace)
Delete supplied singular-factor rows with the full-spectrum symmetric covariance core.
svdDeletionBackend
Numerical backend used to delete rows or columns from thin-SVD factors.
Eigen::Array< realT, Eigen::Dynamic, 1 > svdDeletionVector
Dynamic column vector used by the SVD deletion API.
const char * svdDeletionBackendName(svdDeletionBackend backend)
Return a stable text representation of an SVD deletion backend.
MXLIB_SVD_DELETION_HEADER_ADAPTER svdDeletionStatus svdRemoveRows(svdDeletionResult< realT > &result, std::type_identity_t< svdDeletionConstVectorRef< realT > > singularValues, std::type_identity_t< svdDeletionConstMatrixRef< realT > > leftFactor, std::span< const Eigen::Index > deletedIndices, Eigen::Index outputRank, svdDeletionWorkspace< realT > &workspace, svdDeletionBackend backend=svdDeletionBackend::stableCore)
Delete physical rows from the matrix represented by a thin SVD.
MXLIB_SVD_DELETION_HEADER_ADAPTER svdDeletionStatus svdDeletionStableCore(svdDeletionResult< realT > &result, std::type_identity_t< svdDeletionConstVectorRef< realT > > singularValues, std::type_identity_t< svdDeletionConstMatrixRef< realT > > deletedRows, Eigen::Index outputRank, svdDeletionWorkspace< realT > &workspace)
Delete supplied singular-factor rows with the complement-preserving small-SVD core.
Eigen::Array< realT, Eigen::Dynamic, Eigen::Dynamic, Eigen::ColMajor > svdDeletionMatrix
Column-major dynamic matrix used by the SVD deletion API.
bool svdDeletionSucceeded(svdDeletionStatus status) noexcept
Return true when a status represents usable numerical output.
ABI-stable borrowed signed-index storage descriptor.
ABI-stable borrowed column-major matrix storage descriptor.
ABI-stable borrowed contiguous-vector storage descriptor.
Reusable row and column deletion updates for thin singular value decompositions.