.. _complex: General complex eigenproblems ============================= This page is a walkthrough showing how to use :ref:`ezarpack::arpack_solver\<Complex, \Backend> <refsolvercomplex>` in your C++ code to compute a few eigenpairs :math:`(\lambda,\mathbf{x})` of .. math:: \hat A \mathbf{x} = \lambda \hat M \mathbf{x} with a complex matrix :math:`\hat A` and a complex Hermitian positive semi-definite matrix :math:`\hat M`. The complex solver class supports a few computational modes, where the original eigenproblem is recast into .. math:: \hat O \mathbf{x} = \mu \mathbf{x}. Operator :math:`\hat O` acts in a vector space equipped with an inner product defined by the matrix :math:`\hat B = \hat M`, .. math:: \langle \mathbf{x}, \mathbf{y} \rangle = \mathbf{x}^\dagger \hat B \mathbf{y}. There are explicit relations between the original eigenvalues :math:`\lambda` and their transformed counterparts :math:`\mu`. .. note:: If both matrices :math:`\hat A` and :math:`\hat M` are real, :ref:`ezarpack::arpack_solver\<Symmetric, \Backend> <refsolversymmetric>` or :ref:`ezarpack::arpack_solver\<Asymmetric, \Backend> <refsolverasymmetric>` should be used instead. Typical steps needed to compute the eigenpairs are as follows. 1. Decide what :ref:`storage backend<backends>` you want to use or whether it is appropriate to :ref:`implement a new one<new_backend>`. In the following, we will assume that the `Eigen <http://eigen.tuxfamily.org>`_ backend has been selected. 2. Include ``<ezarpack/arpack_solver.hpp>`` and the relevant backend header. .. code:: #include <ezarpack/arpack_solver.hpp> #include <ezarpack/storages/eigen.hpp> .. note:: ``<ezarpack/arpack_solver.hpp>`` includes :ref:`all three specializations of ezarpack::arpack_solver<refsolver>` at once. If you want to speed up compilation a little bit, you can include ``<ezarpack/solver_base.hpp>`` and ``<ezarpack/solver_complex.hpp>`` instead. 3. Create a solver object. .. code:: cpp const int N = 1000; // Size of matrices A and M using namespace ezarpack; // Shorthand for solver's type. using solver_t = arpack_solver<Complex, eigen_storage>; // Solver object. solver_t solver(N); 4. Fill a ``params_t`` structure with calculation parameters. .. code:: cpp // params_t is a structure holding parameters of // the Implicitly Restarted Arnoldi iteration. using params_t = solver_t::params_t; // Requested number of eigenvalues to compute. const int nev = 10; // Compute the eigenvalues with the largest magnitudes. auto eigenvalues_select = params_t::LargestMagnitude; // Compute Ritz vectors (eigenvectors). params_t::compute_vectors_t compute_vectors = params_t::Ritz; // params_t's constructor takes three arguments -- mandatory parameters // that need be set explicitly. params_t params(nev, eigenvalues_select, compute_vectors); The following table contains an annotated list of all supported parameters. .. _params: .. list-table:: :header-rows: 1 :align: left :widths: auto * - Parameter name - Type - Default value - Description .. _complex_n_eigenvalues: * - ``n_eigenvalues`` - ``unsigned int`` - n/a - Number of eigenvalues to compute. .. _complex_eigenvalues_select: * - ``eigenvalues_select`` - ``params_t::eigenvalues_select_t`` (enumeration) - n/a - Part of the spectrum to target. Acceptable values are ``LargestMagnitude`` (largest eigenvalues in magnitude), ``SmallestMagnitude`` (smallest eigenvalues in magnitude), ``LargestReal`` (eigenvalues of largest real part), ``SmallestReal`` (eigenvalues of smallest real part), ``LargestImag`` (eigenvalues of largest imaginary part) and ``SmallestImag`` (eigenvalues of smallest imaginary part). .. _complex_ncv: * - ``ncv`` - ``int`` - min(2 * ``n_eigenvalues`` + 2, ``N``) - How many Arnoldi vectors to generate at each iteration. .. _complex_compute_vectors: * - ``compute_vectors`` - ``compute_vectors_t`` (enumeration) - n/a - ``Schur`` -- compute only Schur vectors (orthogonal basis vectors of the ``n_eigenvalues``-dimensional subspace), ``Ritz`` -- compute Ritz vectors (eigenvectors) in addition to the Schur vectors, ``None`` -- compute neither Schur nor Ritz vectors. .. _complex_random_residual_vector: * - ``random_residual_vector`` - ``bool`` - ``true`` - Use a randomly generated initial residual vector? .. _complex_sigma: * - ``sigma`` - ``std::complex<double>`` - `0` - Complex eigenvalue shift :math:`\sigma` for spectral transformation modes. .. _complex_tolerance: * - ``tolerance`` - ``double`` - Machine precision - Relative tolerance for Ritz value (eigenvalue) convergence. .. _complex_max_iter: * - ``max_iter`` - ``unsigned int`` - ``INT_MAX`` - Maximum number of Arnoldi update iterations allowed. .. note:: In the Shift-and-Invert mode, values of :ref:`eigenvalues_select <complex_eigenvalues_select>` refer to the spectrum of the **transformed** problem, not the original one. For instance, ``LargestMagnitude`` with a complex shift :math:`\sigma` will pick eigenvalues :math:`\lambda` closest to :math:`\sigma`, because they correspond to the eigenvalues :math:`\mu = 1/(\lambda - \sigma)` that have the largest magnitude. 5. Optionally set the initial vector for Arnoldi iteration if a better choice than a random vector is known. :ref:`random_residual_vector <complex_random_residual_vector>` parameter must be set to ``false`` for the changes made to the initial vector to take effect. A view of the residual vector is accessible via method ``residual_vector()`` of the solver. .. code:: cpp // Set all components of the initial vector to 1. auto rv = solver.residual_vector(); for(int i = 0; i < N; ++i) rv[i] = 1.0; One may also call ``residual_vector()`` later, after a diagonalization run has started, to retrieve the current residual vector. 6. Choose one of supported computational modes and perform diagonalization. In this part, user is supposed to call the ``solver`` object and pass the parameter structure as well as callable objects (*e.g.* lambda-functions) that represent action of operators :math:`\hat O` and :math:`\hat B` on a given vector. The supplied objects will be called to generate Arnoldi vectors. Syntax and semantics of the C++ code vary between the computational modes and will be explained individually for each of them. .. _complex_standard: - **Standard mode** (for standard eigenproblems, :math:`\hat M = \hat I`). .. code:: cpp using vector_view_t = solver_t::vector_view_t; using vector_const_view_t = solver_t::vector_const_view_t; auto Aop = [](vector_const_view_t in, vector_view_t out) { // Code implementing action of matrix A on vector 'in': // out = A * in }; solver(Aop, params); .. _complex_Inverse: - **Regular inverse mode** (for positive-definite :math:`\hat M`). In this mode, the transformed eigenproblem is defined by :math:`\hat O = \hat M^{-1} \hat A`, :math:`\hat B = \hat M` and :math:`\lambda = \mu`. .. code:: cpp using vector_view_t = solver_t::vector_view_t; using vector_const_view_t = solver_t::vector_const_view_t; auto op = [](vector_const_view_t in, vector_view_t out) { // Code implementing action of matrix M^{-1} A on vector 'in': // out = M^{-1} * A * in }; auto Bop = [](vector_const_view_t in, vector_view_t out) { // Code implementing action of matrix M on vector 'in': // out = M * in }; solver(op, Bop, solver_t::Inverse, params); Inverting a sparse matrix :math:`\hat M` will likely make it dense, which is usually undesirable from the storage standpoint. A more practical solution is to compute the sparse LU or Cholesky factorization of :math:`\hat M` once (outside of the lambda-function's body), and write the lambda-function so that it computes ``out`` as the solution of the linear system ``M * out = A * in`` using the precomputed factorization. .. _complex_ShiftAndInvert: - **Shift-and-Invert mode**. In this mode, the transformed eigenproblem is defined by :math:`\hat O = (\hat A -\sigma \hat M)^{-1} \hat M`, :math:`\hat B = \hat M` and :math:`\lambda = 1/\mu + \sigma`. The complex spectral shift :math:`\sigma` must be set in the parameters structure, see the :ref:`list of parameters <complex_sigma>`. .. code:: cpp using vector_view_t = solver_t::vector_view_t; using vector_const_view_t = solver_t::vector_const_view_t; auto op = [](vector_view_t in, vector_view_t out) { // Code implementing action of matrix (A - sigma*M)^{-1} * M on 'in': // out = (A - sigma*M)^{-1} * M * in }; auto Bop = [](vector_const_view_t in, vector_view_t out) { // Code implementing action of matrix M on vector 'in': // out = M * in }; solver(op, Bop, solver_t::ShiftAndInvert, params); Inverting a sparse matrix :math:`\hat A - \sigma\hat M` will likely make it dense, which is usually undesirable from the storage standpoint. A more practical solution is to compute the sparse LU or Cholesky factorization of :math:`\hat A - \sigma\hat M` once (outside of the lambda-function's body), and write the lambda-function so that it (1) computes ``M * in`` and (2) computes ``out`` as the solution of the linear system ``(A - \sigma M) * out = M * in`` using the precomputed factorization. .. note:: In most computational modes above, it is seemingly necessary to apply operator :math:`\hat B` to the same vector twice per generated Arnoldi vector, once in functor ``op`` and once in ``Bop``. It is actually possible to spare one of the applications. Calling ``solver.Bx_available()`` inside ``op`` will tell whether ``Bop`` has already been called at the current iteration, and ``solver.Bx_vector()`` will return a constant view of the application result :math:`\hat B \mathbf{x}`. The ``in`` and ``out`` views passed to the callable objects always expose one of three length-:math:`N` vectors stored inside the solver object. There is another, indirect way to access them. .. code:: cpp // Get index (0-2) of the current 'in' vector and request a view of it auto in_view = solver.workspace_vector(solver.in_vector_n()); // Similar for the 'out' vector auto out_view = solver.workspace_vector(solver.out_vector_n()); In advanced usage scenarios, the implicit restarting procedure can be customized via an extra argument of ``solver``'s call operator. See ':ref:`restarting`' for more details. .. code:: cpp auto shifts_f = [](solver_t::complex_vector_const_view_t ritz_values, solver_t::complex_vector_const_view_t ritz_bounds, solver_t::complex_vector_view_t shifts) { // Compute shifts for the implicit restarting }; // Standard mode solver(op, params, shifts_f); // Other modes, e.g. Inverse solver(op, Bop, solver_t::Inverse, params, shifts_f); ``solver_t::operator()`` can throw two special exception types. - :cpp:type:`ezarpack::maxiter_reached` - Maximum number of implicitly restarted Arnoldi iterations has been reached. - :cpp:type:`ezarpack::ncv_insufficient` - No shifts could be applied during a cycle of the Implicitly restarted Arnoldi iteration. Consider increasing the number of Arnoldi vectors generated at each iteration (:ref:`ncv <complex_ncv>` parameter). The rest of possible problems reported by ARPACK-NG result in generic `std::runtime_error <https://en.cppreference.com/w/cpp/error/runtime_error>`_ exceptions. 7. Request computed eigenvalues and eigenvectors. For the eigenvectors, the :ref:`compute_vectors <complex_compute_vectors>` parameter must be set to ``params_t::Ritz``. .. code:: cpp auto lambda = solver.eigenvalues(); auto vecs = solver.eigenvectors(); The eigenvectors are columns of the complex matrix view ``vecs``. If the diagonalization run has ended prematurely (for example, when the maximum number of iterations has been reached), then it is still possible to extract ``solver.nconv()`` converged eigenpairs. 8. Optionally request the Schur vectors, i.e. :math:`\hat B`-orthogonal basis vectors of the relevant vector subspace (:ref:`compute_vectors <complex_compute_vectors>` must be either ``params_t::Schur`` or ``params_t::Ritz``). .. code:: cpp auto basis = solver.schur_vectors(); The basis vectors are ``solver.nconv()`` columns of the complex matrix view ``basis``. 9. Optionally request statistics about the completed run. .. code:: cpp // Print some computation statistics auto stats = solver.stats(); std::cout << "Number of Arnoldi update iterations: " << stats.n_iter << std::endl; std::cout << "Total number of O*x operations: " << stats.n_op_x_operations << std::endl; std::cout << "Total number of B*x operations: " << stats.n_b_x_operations << std::endl; std::cout << "Total number of steps of re-orthogonalization: " << stats.n_reorth_steps << std::endl;