/*---------------------------------------------------------------------------*\ ========= | \\ / F ield | OpenFOAM: The Open Source CFD Toolbox \\ / O peration | \\ / A nd | www.openfoam.com \\/ M anipulation | ------------------------------------------------------------------------------- Copyright (C) 2020-2022 OpenCFD Ltd. ------------------------------------------------------------------------------- License This file is derivative work of OpenFOAM. OpenFOAM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. OpenFOAM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenFOAM. If not, see . Application Test-EigenMatrix Description Tests for \c EigenMatrix constructors, and member functions using \c floatScalar, and \c doubleScalar base types. Cross-checks were obtained from 'NumPy 1.15.1' if no theoretical cross-check exists (like eigendecomposition relations), and were hard-coded for elementwise comparisons. \*---------------------------------------------------------------------------*/ #include "scalarMatrices.H" #include "RectangularMatrix.H" #include "SquareMatrix.H" #include "complex.H" #include "IOmanip.H" #include "EigenMatrix.H" #include "TestTools.H" using namespace Foam; // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // // Create each constructor of EigenMatrix, and print output template void test_constructors(Type) { { Info<< "# Construct from a SquareMatrix" << nl; const SquareMatrix A(5, Zero); const EigenMatrix EM(A); } { Info<< "# Construct from a SquareMatrix and symmetry flag" << nl; const SquareMatrix A(5, Zero); const EigenMatrix EM(A, true); } } // Execute each member function of EigenMatrix, and print output template void test_member_funcs(Type) { SquareMatrix A(3, Zero); assignMatrix ( A, { Type(1), Type(2), Type(3), Type(4), Type(5), Type(6), Type(7), Type(8), Type(9) } ); Info<< "# Operand: " << nl << " SquareMatrix = " << A << endl; // Since eigenvalues are unique and eigenvectors are not unique, // the bitwise comparisons are limited to eigenvalue computations. // Here, only the execution of the functions is tested, rather than // the verification of the eigendecomposition through theoretical relations. { const EigenMatrix EM(A); const DiagonalMatrix EValsRe = EM.EValsRe(); const DiagonalMatrix& EValsIm = EM.EValsIm(); const SquareMatrix& EVecs = EM.EVecs(); cmp ( " Return real eigenvalues or real part of complex eigenvalues = ", EValsRe, List ({ Type(16.116844), Type(-1.116844), Type(0) }), getTol(Type(0)), 1e-6 ); cmp ( " Return zero-matrix for real eigenvalues " "or imaginary part of complex eigenvalues = ", EValsIm, List(3, Zero) ); Info<< " Return eigenvectors matrix = " << EVecs << endl; } } // Test the relation: "sum(eigenvalues) = trace(A)" // w.wiki/4zs (Retrieved: 16-06-19) # Item-1 template void test_eigenvalues_sum ( const SquareMatrix& A, const DiagonalMatrix& EValsRe ) { const Type trace = A.trace(); // Imaginary part of complex conjugates cancel each other const Type EValsSum = sum(EValsRe); Info<< " # A.mRows = " << A.m() << nl; cmp ( " # sum(eigenvalues) = trace(A) = ", EValsSum, trace, getTol(Type(0)) ); } // Test the relation: "prod(eigenvalues) = det(A)" // w.wiki/4zs (Retrieved: 16-06-19) # Item-2 // Note that the determinant computation may fail // which is not a suggestion that eigendecomposition fails template void test_eigenvalues_prod ( const SquareMatrix& A, const DiagonalMatrix& EValsRe, const DiagonalMatrix& EValsIm ) { const Type determinant = mag(det(A)); Type EValsProd = Type(1); if (EValsIm.empty()) { for (label i = 0; i < EValsRe.size(); ++i) { EValsProd *= Foam::sqrt(sqr(EValsRe[i])); } } else { for (label i = 0; i < EValsRe.size(); ++i) { EValsProd *= Foam::sqrt(sqr(EValsRe[i]) + sqr(EValsIm[i])); } } cmp ( " # prod(eigenvalues) = det(A) = ", EValsProd, determinant, getTol(Type(0)) ); } // Test eigenvalues in eigendecomposition relations // Relations: (Beauregard & Fraleigh (1973), ISBN 0-395-14017-X, p. 307) template void test_eigenvalues(Type) { Random rndGen(1234); const label numberOfTests = 20; // Non-symmetric for (label i = 0; i < numberOfTests; ++i) { const label mRows = rndGen.position(100, 200); const labelPair m(mRows, mRows); const SquareMatrix A ( makeRandomMatrix>(m, rndGen) ); const EigenMatrix EM(A); const DiagonalMatrix& EValsRe = EM.EValsRe(); test_eigenvalues_sum(A, EValsRe); // LUDecompose does not work with floatScalar at the time of writing, // hence det function. Once LUDecompose is refactored, comment out below // const DiagonalMatrix& EValsIm = EM.EValsIm(); // test_eigenvalues_prod(A, EValsRe, EValsIm); } // Symmetric for (label i = 0; i < numberOfTests; ++i) { const label mRows = rndGen.position(100, 200); const labelPair m(mRows, mRows); SquareMatrix A ( makeRandomMatrix>(m, rndGen) ); // Symmetrise with noise for (label n = 0; n < A.n() - 1; ++n) { for (label m = A.m() - 1; m > n; --m) { A(n, m) = A(m, n) + SMALL; } } const bool symmetric = true; const EigenMatrix EM(A, symmetric); const DiagonalMatrix& EValsRe = EM.EValsRe(); test_eigenvalues_sum(A, EValsRe); } } // Test the relation: "(A & EVec - EVal*EVec) = 0" template void test_characteristic_eq ( const SquareMatrix& Aorig, const DiagonalMatrix& EValsRe, const DiagonalMatrix& EValsIm, const SquareMatrix& EVecs ) { SquareMatrix A(Aorig.m()); auto convertToComplex = [&](const scalar& val) { return complex(val); }; std::transform ( Aorig.cbegin(), Aorig.cend(), A.begin(), convertToComplex ); for (label i = 0; i < A.m(); ++i) { const RectangularMatrix& EVec(EVecs.subColumn(i)); const complex EVal(EValsRe[i], EValsIm[i]); const RectangularMatrix leftSide(A*EVec); const RectangularMatrix rightSide(EVal*EVec); cmp ( " # (A & EVec - EVal*EVec) = 0:", flt(leftSide), flt(rightSide), getTol(Type(0)) ); } } // Test eigenvectors in eigendecomposition relations template void test_eigenvectors(Type) { Random rndGen(1234); const label numberOfTests = 20; // Non-symmetric for (label i = 0; i < numberOfTests; ++i) { const label mRows = rndGen.position(100, 200); const labelPair m(mRows, mRows); const SquareMatrix A ( makeRandomMatrix>(m, rndGen) ); const EigenMatrix EM(A); const DiagonalMatrix& EValsRe = EM.EValsRe(); const DiagonalMatrix& EValsIm = EM.EValsIm(); const SquareMatrix EVecs(EM.complexEVecs()); test_characteristic_eq(A, EValsRe, EValsIm, EVecs); } // Symmetric for (label i = 0; i < numberOfTests; ++i) { const label mRows = rndGen.position(100, 200); const labelPair m(mRows, mRows); SquareMatrix A ( makeRandomMatrix>(m, rndGen) ); // Symmetrise with noise for (label n = 0; n < A.n() - 1; ++n) { for (label m = A.m() - 1; m > n; --m) { A(n, m) = A(m, n) + SMALL; } } const bool symmetric = true; const EigenMatrix EM(A, symmetric); const DiagonalMatrix& EValsRe = EM.EValsRe(); const DiagonalMatrix& EValsIm = EM.EValsIm(); const SquareMatrix EVecs(EM.complexEVecs()); test_characteristic_eq(A, EValsRe, EValsIm, EVecs); } } // Do compile-time recursion over the given types template inline typename std::enable_if::type run_tests(const std::tuple& types, const List& typeID){} template inline typename std::enable_if::type run_tests(const std::tuple& types, const List& typeID) { Info<< nl << " ## Test constructors: "<< typeID[I] <<" ##" << nl; test_constructors(std::get(types)); Info<< nl << " ## Test member functions: "<< typeID[I] <<" ##" << nl; test_member_funcs(std::get(types)); Info<< nl << " ## Test eigenvalues: "<< typeID[I] <<" ##" << nl; test_eigenvalues(std::get(types)); Info<< nl << " ## Test eigenvectors: "<< typeID[I] <<" ##" << nl; test_eigenvectors(std::get(types)); run_tests(types, typeID); } // * * * * * * * * * * * * * * * Main Program * * * * * * * * * * * * * * * // int main() { Info<< setprecision(15); const std::tuple types ( std::make_tuple(Zero, Zero) ); const List typeID ({ "SquareMatrix", "SquareMatrix" }); run_tests(types, typeID); Info<< nl << " ## Test corner cases ##" << endl; { Info<< nl << " ## Rosser et al. (1951) matrix: ##" << nl; // Rosser, J. B., Lanczos, C., Hestenes, M. R., & Karush, W. (1951). // Separation of close eigenvalues of a real symmetric matrix. // Jour. Research of the National Bureau of Standards, 47(4), 291-297. // DOI:10.6028/jres.047.037 // // 8x8 symmetric square matrix consisting of close real eigenvalues // ibid, p. 294 // { // 1020.04901843, 1020.000, 1019.90195136, 1000.000, // 1000.000, 0.09804864072, 0.000, -1020.0490 // } // Note that prod(eigenvalues) != determinant(A) for this matrix // via the LAPACK routine z/dgetrf SquareMatrix A(8, Zero); assignMatrix ( A, { 611, 196, -192, 407, -8, -52, -49, 29, 196, 899, 113, -192, -71, -43, -8, -44, -192, 113, 899, 196, 61, 49, 8, 52, 407, -192, 196, 611, 8, 44, 59, -23, -8, -71, 61, 8, 411, -599, 208, 208, -52, -43, 49, 44, -599, 411, 208, 208, -49, -8, 8, 59, 208, 208, 99, -911, 29, -44, 52, -23, 208, 208, -911, 99 } ); const EigenMatrix EM(A); const DiagonalMatrix& EValsRe = EM.EValsRe(); const DiagonalMatrix& EValsIm = EM.EValsIm(); test_eigenvalues_sum(A, EValsRe); cmp ( " # Rosser et al. (1951) case, EValsRe = ", EValsRe, List // theoretical EValsRe ({ -1020.0490, 0.000, 0.09804864072, 1000.000, 1000.000, 1019.90195136, 1020.000, 1020.04901843 }), 1e-3 ); cmp ( " # Rosser et al. (1951) case, EValsIm = ", EValsIm, List(8, Zero) ); } { Info<< nl << " ## Test eigenvector unpacking: ##" << nl; SquareMatrix A(3, Zero); assignMatrix ( A, { 1, 2, 3, -4, -5, 6, 7, -8, 9 } ); const EigenMatrix EM(A); const SquareMatrix complexEVecs(EM.complexEVecs()); SquareMatrix B(3, Zero); assignMatrix ( B, { complex(-0.373220280), complex(0.417439996, 0.642691344), complex(0.417439996, -0.642691344), complex(-0.263919251), complex(-1.165275867, 0.685068715), complex(-1.165275867, -0.685068715), complex(-0.889411744), complex(-0.89990601, -0.3672785281), complex(-0.89990601, 0.3672785281), } ); cmp ( " # ", flt(complexEVecs), flt(B) ); } { Info<< nl << " ## Test matrices with small values: ##" << nl; const List epsilons ({ 0, SMALL, Foam::sqrt(SMALL), sqr(SMALL), Foam::cbrt(SMALL), -SMALL, -Foam::sqrt(SMALL), -sqr(SMALL), -Foam::cbrt(SMALL) }); Random rndGen(1234); const label numberOfTests = 20; for (label i = 0; i < numberOfTests; ++i) { const label mRows = rndGen.position(100, 200); for (const auto& eps : epsilons) { const SquareMatrix A(mRows, eps); const EigenMatrix EM(A); const DiagonalMatrix& EValsRe = EM.EValsRe(); const DiagonalMatrix& EValsIm = EM.EValsIm(); const SquareMatrix EVecs(EM.complexEVecs()); test_eigenvalues_sum(A, EValsRe); test_characteristic_eq(A, EValsRe, EValsIm, EVecs); } } } { Info<< nl << " ## Test matrices with repeating eigenvalues: ##" << nl; SquareMatrix A(3, Zero); assignMatrix ( A, { 0, 1, 1, 1, 0, 1, 1, 1, 0 } ); const EigenMatrix EM(A); const DiagonalMatrix& EValsRe = EM.EValsRe(); const DiagonalMatrix& EValsIm = EM.EValsIm(); const SquareMatrix EVecs(EM.complexEVecs()); test_eigenvalues_sum(A, EValsRe); test_characteristic_eq(A, EValsRe, EValsIm, EVecs); } if (nFail_) { Info<< nl << " #### " << "Failed in " << nFail_ << " tests " << "out of total " << nTest_ << " tests " << "####\n" << endl; return 1; } Info<< nl << " #### Passed all " << nTest_ <<" tests ####\n" << endl; return 0; }