Skip to content

04_eigen_and_svd.cpp

Symmetric eigendecomposition, Lanczos, power iteration and the SVD.

eig_sym returns every eigenpair of a matrix that claims the self-adjoint law. lanczos finds the k largest eigenvalues from matrix-vector products alone, and power_iteration finds the dominant one. svd returns the singular values of any matrix. The 1D Laplacian has eigenvalues \(2 - 2\cos(k\pi/(n+1))\), so the output can be checked against them.

#include <cmath>
#include <cstdio>
#include <numbers>
#include <numerics.hpp>
#include <string_view>
#include <vector>

using namespace num;

int main(int argc, char **argv) {
    const bool plot = argc > 1 && std::string_view(argv[1]) == "--plot";
    constexpr idx n = 100;
    mat<real> L(n, n, 0.0);
    for (idx i = 0; i < n; ++i) {
        L(i, i) = 2.0;
        if (i + 1 < n) {
            L(i, i + 1) = L(i + 1, i) = -1.0;
        }
    }
    auto exact = [](idx k) {
        return 2.0 - (2.0 * std::cos(static_cast<real>(k) * std::numbers::pi / (n + 1)));
    };

    const eigen_result all = eig_sym(assume_symmetric(L));
    std::printf("eig_sym     smallest %.8f  largest %.8f  (exact %.8f, %.8f)\n", all.values[0],
                all.values[n - 1], exact(1), exact(n));

    // Lanczos stops after max_steps (default min(3k, n)) and reports whether the Ritz values
    // met the tolerance. The largest eigenvalues of L are clustered, so the default is too few.
    for (idx max_steps : {0, 100}) {
        const lanczos_result top = lanczos(assume_symmetric(L), 3, 1e-10, max_steps);
        std::printf("lanczos     3 largest %.8f %.8f %.8f  %3zu steps, converged %d\n",
                    top.ritz_values[2], top.ritz_values[1], top.ritz_values[0],
                    static_cast<std::size_t>(top.steps), top.converged);
    }
    std::printf("            exact     %.8f %.8f %.8f\n", exact(n), exact(n - 1), exact(n - 2));

    const power_result dominant = power_iteration(L, 1e-10, 20000);
    std::printf("power       dominant %.8f  in %zu iterations\n", dominant.eigenvalue,
                static_cast<std::size_t>(dominant.iterations));

    // The singular values of a 4 x 3 matrix.
    mat<real> B(4, 3, 0.0);
    for (idx i = 0; i < 4; ++i) {
        for (idx j = 0; j < 3; ++j) {
            B(i, j) = static_cast<real>(i + 1) / static_cast<real>(j + 1 + i);
        }
    }
    const svd_result result = svd(B);
    std::printf("svd         singular values %.6f %.6f %.6f\n", result.S[0], result.S[1], result.S[2]);

    if (plot) {
        std::vector<double> k, numeric, analytic;
        for (idx i = 0; i < n; ++i) {
            k.push_back(static_cast<double>(i + 1));
            numeric.push_back(all.values[i]);
            analytic.push_back(exact(i + 1));
        }
        plt::plot(k, numeric, "eig_sym", "points");
        plt::plot(k, analytic, "2 - 2 cos(k pi / (n+1))", "lines");
        plt::title("04 Spectrum of the 1D Laplacian");
        plt::show();
    }
}

Possible output:

eig_sym     smallest 0.00096744  largest 3.99903256  (exact 0.00096744, 3.99903256)
lanczos     3 largest 3.90211303 3.61803399 3.17557050    9 steps, converged 0
lanczos     3 largest 3.99903256 3.99613119 3.99129870  100 steps, converged 1
            exact     3.99903256 3.99613119 3.99129870
power       dominant 3.99903229  in 7337 iterations
svd         singular values 2.644853 0.259121 0.010318