packages feed

linear-massiv-0.1.0.1: src/Numeric/LinearAlgebra/Massiv/Eigen/Power.hs

{-# LANGUAGE AllowAmbiguousTypes #-}

-- |
-- Module      : Numeric.LinearAlgebra.Massiv.Eigen.Power
-- Copyright   : (c) Nadia Chambers 2026
-- License     : BSD-3-Clause
-- Maintainer  : nadia.chambers@iohk.io
-- Stability   : experimental
--
-- Power iteration methods for computing individual eigenvalue\/eigenvector
-- pairs of a general square matrix.
--
-- This module implements three iterative projection techniques drawn from
-- Golub & Van Loan, /Matrix Computations/, 4th edition (GVL4), Section 7.3,
-- pp. 372--382:
--
-- * __Power method__ (Algorithm 7.3.3, p. 375) — converges to the dominant
--   eigenpair at a rate governed by the ratio \(|\lambda_2 / \lambda_1|\) per
--   iteration, where \(\lambda_1\) is the eigenvalue of largest modulus.
--
-- * __Inverse iteration__ (Section 7.3.1, p. 377) — given a shift \(\mu\),
--   converges to the eigenvalue closest to \(\mu\) by applying the power
--   method to \((A - \mu I)^{-1}\).
--
-- * __Rayleigh quotient iteration__ (Section 7.3.2, p. 379) — an adaptive
--   variant of inverse iteration in which the shift is updated at every step
--   to equal the current Rayleigh quotient.  For symmetric matrices this
--   achieves /cubic/ convergence; for general matrices the convergence is
--   /quadratic/.
--
-- All three routines return an approximate eigenvalue \(\lambda\) and its
-- associated eigenvector \(q\) satisfying \(Aq \approx \lambda q\).
module Numeric.LinearAlgebra.Massiv.Eigen.Power
  ( -- * Power method (Algorithm 7.3.3)
    powerMethod
    -- * Inverse iteration (Section 7.3.1)
  , inverseIteration
    -- * Rayleigh quotient iteration (Section 7.3.2)
  , rayleighQuotient
  ) where

import qualified Data.Massiv.Array as M
import GHC.TypeNats (KnownNat)

import Numeric.LinearAlgebra.Massiv.Types
import Numeric.LinearAlgebra.Massiv.Internal
import Numeric.LinearAlgebra.Massiv.BLAS.Level1 (dot, scal, nrm2)
import Numeric.LinearAlgebra.Massiv.BLAS.Level2 (matvec)
import Numeric.LinearAlgebra.Massiv.Solve.LU (luSolve)

-- | Power method for computing the dominant eigenpair (GVL4 Algorithm 7.3.3,
-- p. 375).
--
-- Given a square matrix \(A \in \mathbb{R}^{n \times n}\) with eigenvalues
-- ordered \(|\lambda_1| > |\lambda_2| \geq \cdots \geq |\lambda_n|\), the
-- power method generates a sequence of vectors
--
-- \[
--   z_k = A q_{k-1}, \qquad q_k = z_k / \|z_k\|_2
-- \]
--
-- that converges to the eigenvector associated with \(\lambda_1\).  The
-- corresponding eigenvalue is estimated via the Rayleigh quotient
-- \(\lambda \approx q_k^T A q_k\).
--
-- __Convergence rate:__ the error contracts by a factor of
-- \(|\lambda_2 / \lambda_1|\) per iteration (GVL4, p. 375).  The method
-- therefore requires a dominant eigenvalue that is well-separated from the
-- rest of the spectrum.
--
-- Returns @(eigenvalue, eigenvector)@ once the eigenvalue estimate changes by
-- less than the given tolerance, or after the specified number of iterations.
powerMethod :: forall n r e. (KnownNat n, M.Manifest r e, Floating e, Ord e)
            => Matrix n n r e
            -> Vector n r e    -- ^ Initial guess \(q_0\) (should be unit norm)
            -> Int             -- ^ Maximum iterations
            -> e               -- ^ Convergence tolerance
            -> (e, Vector n r e)
powerMethod a q0 maxIter tol = go 0 q0 0
  where
    go :: Int -> Vector n r e -> e -> (e, Vector n r e)
    go iter q prevLambda
      | iter >= maxIter = (prevLambda, q)
      | otherwise =
        let z = matvec a q                        -- z = A·q
            znorm = nrm2 z                        -- ‖z‖₂
            qNew = scal (1 / znorm) z             -- q = z / ‖z‖₂
            lambda = dot qNew (matvec a qNew)     -- λ = qᵀAq (Rayleigh quotient)
        in if abs (lambda - prevLambda) < tol
           then (lambda, qNew)
           else go (iter + 1) qNew lambda

-- | Inverse iteration for computing the eigenpair closest to a given shift
-- (GVL4 Section 7.3.1, p. 377).
--
-- Given a shift \(\mu\) that approximates some eigenvalue of \(A\), inverse
-- iteration applies the power method to the matrix \((A - \mu I)^{-1}\).
-- Each step solves the linear system
--
-- \[
--   (A - \mu I)\, z_k = q_{k-1}, \qquad q_k = z_k / \|z_k\|_2
-- \]
--
-- and converges to the eigenvalue \(\lambda_j\) that minimises
-- \(|\lambda_j - \mu|\).  The convergence rate is
-- \(|\lambda_j - \mu| / |\lambda_i - \mu|\) per iteration, where
-- \(\lambda_i\) is the second-closest eigenvalue to \(\mu\).
--
-- The eigenvalue estimate is refined at each step via the Rayleigh quotient
-- \(\lambda \approx q_k^T A q_k\) (using the /original/ matrix \(A\)).
--
-- Returns @(eigenvalue, eigenvector)@.
inverseIteration :: forall n r e. (KnownNat n, M.Manifest r e, Floating e, Ord e)
                 => Matrix n n r e
                 -> e               -- ^ Shift \(\mu\)
                 -> Vector n r e    -- ^ Initial guess
                 -> Int             -- ^ Maximum iterations
                 -> e               -- ^ Convergence tolerance
                 -> (e, Vector n r e)
inverseIteration a mu q0 maxIter tol = go 0 q0 0
  where
    aShifted = makeMatrix @n @n @r $ \i j ->
      if i == j then (a ! (i, j)) - mu else a ! (i, j)

    go :: Int -> Vector n r e -> e -> (e, Vector n r e)
    go iter q prevLambda
      | iter >= maxIter = (prevLambda, q)
      | otherwise =
        let z = luSolve aShifted q                -- Solve (A - μI)z = q
            znorm = nrm2 z
            qNew = scal (1 / znorm) z
            lambda = dot qNew (matvec a qNew)     -- Rayleigh quotient with original A
        in if abs (lambda - prevLambda) < tol
           then (lambda, qNew)
           else go (iter + 1) qNew lambda

-- | Rayleigh quotient iteration (GVL4 Section 7.3.2, p. 379).
--
-- An adaptive variant of inverse iteration in which the shift \(\mu_k\) is
-- set equal to the current Rayleigh quotient at every step:
--
-- \[
--   \mu_k = q_k^T A q_k, \qquad (A - \mu_k I)\, z_{k+1} = q_k, \qquad
--   q_{k+1} = z_{k+1} / \|z_{k+1}\|_2
-- \]
--
-- __Convergence:__
--
-- * For /symmetric/ matrices \(A = A^T\), the iteration converges
--   /cubically/ — the residual \(\|Aq - \lambda q\|\) is cubed at each step
--   (GVL4, p. 379).
-- * For general (non-symmetric) matrices, convergence is /quadratic/.
--
-- Because the shift changes at each iteration, a fresh LU factorisation of
-- \(A - \mu_k I\) is computed every step.  Despite this extra cost the rapid
-- convergence usually makes Rayleigh quotient iteration the method of choice
-- when a good initial vector is available.
--
-- Returns @(eigenvalue, eigenvector)@.
rayleighQuotient :: forall n r e. (KnownNat n, M.Manifest r e, Floating e, Ord e)
                 => Matrix n n r e
                 -> Vector n r e    -- ^ Initial guess
                 -> Int             -- ^ Maximum iterations
                 -> e               -- ^ Convergence tolerance
                 -> (e, Vector n r e)
rayleighQuotient a q0 maxIter tol = go 0 q0 (dot q0 (matvec a q0))
  where
    go :: Int -> Vector n r e -> e -> (e, Vector n r e)
    go iter q lambda
      | iter >= maxIter = (lambda, q)
      | otherwise =
        let aShifted = makeMatrix @n @n @r $ \i j ->
              if i == j then (a ! (i, j)) - lambda else a ! (i, j)
            z = luSolve aShifted q
            znorm = nrm2 z
            qNew = scal (1 / znorm) z
            lambdaNew = dot qNew (matvec a qNew)
        in if abs (lambdaNew - lambda) < tol
           then (lambdaNew, qNew)
           else go (iter + 1) qNew lambdaNew