Search code examples
c++eigen

Eigen 3.3 SVD.solve returning wrong values


After updating from Eigen 3.2.7 to 3.3.4 I encountered an issue with JacobiSVD.solve where it returns a very wrong result. BDCSVD produces the same result.

The issue can be reproduced with the following code:

#include <Eigen/Eigen>
#include <Eigen/SVD>

int main(int argc, const char * argv[]) {

    // M is calculated beforehand and does not change with different Eigen versions
    Eigen::Matrix<double, 12, 12> M = Eigen::Matrix<double, 12, 12>::Zero();
    M(0,0) = 27; M(0,2) = 4.3625039999999995; M(0,3) = -9; M(0,5) = -2.1812519999999997; M(0,6) = -9;
    M(1,1) = 27; M(1,2) = 3.2718720000000001; M(1,4) = -9; M(1,7) = -9; M(1,8) = -1.6359360000000001;
    M(2,0) = 4.3625039999999995; M(2,1) = 3.2718720000000001; M(2,2) = 2.4780489612000003; 
    M(2,3) = -2.1812519999999997; M(2,5) = -0.82601632039999995; M(2,7) = -1.6359360000000001;
    M(2,8) = -0.82601632039999995; M(3,0) = -9; M(3,2) = -2.1812519999999997; M(3,3) = 9;
    M(4,1) = -9; M(4,4) = 9; M(5,0) = -2.1812519999999997; M(5,2) = -0.82601632039999995;
    M(5,5) = 0.82601632039999995; M(6,0) = -9; M(6,6) = 9; M(7,1) = -9; M(7,2) = -1.6359360000000001; 
    M(7,7) = 9; M(8,1) = -1.6359360000000001; M(8,2) = -0.82601632039999995; M(8,8) = 0.82601632039999995;

    Eigen::JacobiSVD<Eigen::MatrixXd> svd(M, Eigen::ComputeFullU);
    Eigen::Matrix<double, 12, 12> ut = svd.matrixU();

    Eigen::Matrix<double, 6, 10> l_6x10;
    Eigen::Matrix<double, 4, 6> dv0;
    Eigen::Matrix<double, 4, 6> dv1;
    Eigen::Matrix<double, 4, 6> dv2;

    for(int i = 0; i < 4; i++) {
        int a = 0, b = 1;

        for(int j = 0; j < 6; j++) {
            dv0(i, j) = ut(3 * a + 0, 11 - i) - ut(3 * b + 0, 11 - i);
            dv1(i, j) = ut(3 * a + 1, 11 - i) - ut(3 * b + 1, 11 - i);
            dv2(i, j) = ut(3 * a + 2, 11 - i) - ut(3 * b + 2, 11 - i);

            b++;
            if (b > 3) {
                a++;
                b = a + 1;
            }
        }
    }

    for(int i = 0; i < 6; i++) {
        l_6x10(i,0) =       (dv0(0, i) * dv0(0, i) + dv1(0, i) * dv1(0, i) + dv2(0, i) * dv2(0, i));
        l_6x10(i,1) = 2.0f * (dv0(0, i) * dv0(1, i) + dv1(0, i) * dv1(1, i) + dv2(0, i) * dv2(1, i));
        l_6x10(i,2) =       (dv0(1, i) * dv0(1, i) + dv1(1, i) * dv1(1, i) + dv2(1, i) * dv2(1, i));
        l_6x10(i,3) = 2.0f * (dv0(0, i) * dv0(2, i) + dv1(0, i) * dv1(2, i) + dv2(0, i) * dv2(2, i));
        l_6x10(i,4) = 2.0f * (dv0(1, i) * dv0(2, i) + dv1(1, i) * dv1(2, i) + dv2(1, i) * dv2(2, i));
        l_6x10(i,5) =       (dv0(2, i) * dv0(2, i) + dv1(2, i) * dv1(2, i) + dv2(2, i) * dv2(2, i));
        l_6x10(i,6) = 2.0f * (dv0(0, i) * dv0(3, i) + dv1(0, i) * dv1(3, i) + dv2(0, i) * dv2(3, i));
        l_6x10(i,7) = 2.0f * (dv0(1, i) * dv0(3, i) + dv1(1, i) * dv1(3, i) + dv2(1, i) * dv2(3, i));
        l_6x10(i,8) = 2.0f * (dv0(2, i) * dv0(3, i) + dv1(2, i) * dv1(3, i) + dv2(2, i) * dv2(3, i));
        l_6x10(i,9) =       (dv0(3, i) * dv0(3, i) + dv1(3, i) * dv1(3, i) + dv2(3, i) * dv2(3, i));
    }

    Eigen::Matrix<double, 6, 4> L_6x4;

    for(int i = 0; i < 6; i++) {
        L_6x4(i, 0) = l_6x10(i, 0);
        L_6x4(i, 1) = l_6x10(i, 1);
        L_6x4(i, 2) = l_6x10(i, 3);
        L_6x4(i, 3) = l_6x10(i, 6);
    }

    // L_6x4 has the following values on 3.2.7 (everything else is 0):
    //
    // L_6x4(0,2) = 1;
    // L_6x4(1,0) = 1;
    // L_6x4(1,1) = 1;
    // L_6x4(5,0) = -1.137432760287006;
    // L_6x4(5,2) = -1.1374327602870071;
    // L_6x4(5,3) = -1.1374327602870049;
    //
    //
    // on 3.3.4 it has the following slightly different values:
    //
    // L_6x4(0,2) = 1;
    // L_6x4(1,0) = 1;
    // L_6x4(1,1) = 1;
    // L_6x4(5,0) = -1.1374327602869998;
    // L_6x4(5,2) = -1.1374327602870271;
    // L_6x4(5,3) = -1.1374327602869889;

    // Rho is calculated beforehand and does not change with different Eigen versions
    Eigen::Matrix<double, 6, 1> Rho;
    Rho << 0.25, 0.140625, 0, 0.390625, 0.25, 0.140625;

    Eigen::JacobiSVD<Eigen::MatrixXd> l_6x4(L_6x4, Eigen::ComputeFullU | Eigen::ComputeFullV);
    Eigen::Vector4d B4 = l_6x4.solve(Rho);

    // The slight difference in L_6x4 apparently causes an insane difference in the result of solve.
    //
    // Eigen 3.2.7: B4={0.056766494562302421, 0, 0, -0.064568070601816963}
    // Eigen 3.3.4: B4={-4773392957911.6992, 0, 0, -4196637484493.9165}
    return 0;
}

The values of B4 calculated with Eigen 3.3.4 make me think that this may either be some weird memory alignment issue, a bug in the SDK or hopefully a bug in this program.

I tried to compile it with -DEIGEN_DONT_ALIGN_STATICALLY or -DEIGEN_DONT_VECTORIZE and -DEIGEN_DISABLE_UNALIGNED_ARRAY_ASSERT. I also tried Eigen::DontAlign on these matrices. None of these have any influence on the result.

Tested on Android(4.4 and 5.1) with ndk-r14b clang c++_static and Mac(macOs Sierra 10.12.6) with: "clang++ -std=c++14 -g -O0 -I/EIGEN_PATH main.cpp -o main"


Solution

  • The two answers are actually very similar similar and yield similar residual. This is because your matrix has two zeros singular values, and a third one that is very close to machine precision. Therefore depending on round-off errors, it is considered to be either 0, in which case you get the minimal norm solution within the remaining 3D subspace of solution:

    {0.056766494562302421, 0, 0, -0.064568070601816963}
    

    and a residual of 0.91. Otherwise it is considered to be meaningful, and then you get the minimal norm solution within the smaller 2D subspace (corresponding to the two zero singular values):

    {-4773392957911.6992, 0, 0, -4196637484493.9165}
    

    with a smaller residual of 0.89. So this second solution is in some sense more accurate, but if you prefer the one with a smaller norm but higher residual, then you can adjust the threshold such that the third singular value is considered to be zero. This is done via setThreshold:

    Eigen::JacobiSVD<Eigen::MatrixXd> l_6x4;
    l_6x4.setThreshold(1e-14);
    l_6x4.compute(L_6x4, Eigen::ComputeFullU | Eigen::ComputeFullV);
    Eigen::Vector4d B4 = l_6x4.solve(Rho);