packages feed

dataframe-learn-2.0.0.0: src-internal/DataFrame/LinearAlgebra/Eigen.hs

{-# LANGUAGE BangPatterns #-}

{- | Symmetric eigenproblems in pure Haskell: cyclic Jacobi for full
decomposition (PCA covariance, @m×m@ kernels) and power iteration for the
dominant eigenpair (FISTA step sizes). Deterministic, sign-canonicalised output.
-}
module DataFrame.LinearAlgebra.Eigen (
    jacobiEigenSym,
    powerIterTop,
) where

import Control.Monad (forM_, when)
import Control.Monad.ST (runST)
import Data.List (sortBy)
import Data.Ord (Down (..), comparing)
import qualified Data.Vector as V
import qualified Data.Vector.Unboxed as VU
import qualified Data.Vector.Unboxed.Mutable as VUM
import DataFrame.LinearAlgebra (Matrix, dot, matVec, scaleV)

{- | Cyclic Jacobi eigendecomposition of a symmetric matrix. Eigenvalues are
returned in descending order paired with eigenvectors as rows, each
sign-canonicalised (largest-magnitude component positive) for unique output.
-}
jacobiEigenSym :: Matrix -> (VU.Vector Double, Matrix)
jacobiEigenSym a0
    | V.null a0 = (VU.empty, V.empty)
    | otherwise = runST $ do
        a <- VUM.new (d * d)
        forM_ [0 .. d - 1] $ \i ->
            forM_ [0 .. d - 1] $ \j ->
                VUM.write a (i * d + j) ((a0 V.! i) VU.! j)
        v <- VUM.replicate (d * d) 0
        forM_ [0 .. d - 1] $ \i -> VUM.write v (i * d + i) 1
        sweep a v 0
        afrozen <- VU.freeze a
        vmat <- VU.freeze v
        let diag = VU.generate d (\i -> afrozen VU.! (i * d + i))
            vecs =
                V.generate d $ \col ->
                    VU.generate d $ \row -> vmat VU.! (row * d + col)
            paired =
                sortBy
                    (comparing (Down . fst))
                    (zip (VU.toList diag) (V.toList vecs))
        pure
            ( VU.fromList (map fst paired)
            , V.fromList (map (canonicalSign . snd) paired)
            )
  where
    d = V.length a0
    maxSweeps = 100
    tol = 1e-12
    sweep a v s
        | s >= maxSweeps = pure ()
        | otherwise = do
            off <- offNorm a
            when (off >= tol) $ do
                forM_ [0 .. d - 2] $ \p ->
                    forM_ [p + 1 .. d - 1] $ \q -> rotate a v p q
                sweep a v (s + 1)
    offNorm a = go 0 0
      where
        go i !acc
            | i >= d = pure acc
            | otherwise = do
                r <- goRow i (i + 1) acc
                go (i + 1) r
        goRow i j !acc
            | j >= d = pure acc
            | otherwise = do
                x <- VUM.read a (i * d + j)
                goRow i (j + 1) (acc + x * x)
    rotate a v p q = do
        apq <- VUM.read a (p * d + q)
        when (abs apq > 1e-300) $ do
            app <- VUM.read a (p * d + p)
            aqq <- VUM.read a (q * d + q)
            let theta = (aqq - app) / (2 * apq)
                s' = if theta == 0 then 1 else signum theta
                t = s' / (abs theta + sqrt (theta * theta + 1))
                c = 1 / sqrt (t * t + 1)
                sn = t * c
            forM_ [0 .. d - 1] $ \i -> do
                aip <- VUM.read a (i * d + p)
                aiq <- VUM.read a (i * d + q)
                VUM.write a (i * d + p) (c * aip - sn * aiq)
                VUM.write a (i * d + q) (sn * aip + c * aiq)
            forM_ [0 .. d - 1] $ \j -> do
                apj <- VUM.read a (p * d + j)
                aqj <- VUM.read a (q * d + j)
                VUM.write a (p * d + j) (c * apj - sn * aqj)
                VUM.write a (q * d + j) (sn * apj + c * aqj)
            forM_ [0 .. d - 1] $ \i -> do
                vip <- VUM.read v (i * d + p)
                viq <- VUM.read v (i * d + q)
                VUM.write v (i * d + p) (c * vip - sn * viq)
                VUM.write v (i * d + q) (sn * vip + c * viq)

canonicalSign :: VU.Vector Double -> VU.Vector Double
canonicalSign vec =
    let idx = VU.maxIndex (VU.map abs vec)
     in if vec VU.! idx < 0 then VU.map negate vec else vec

{- | Dominant eigenvalue and eigenvector of a symmetric PSD matrix via power
iteration with a deterministic all-ones start.
-}
powerIterTop :: Int -> Matrix -> (Double, VU.Vector Double)
powerIterTop iters a
    | V.null a = (0, VU.empty)
    | otherwise = go iters (normalize (VU.replicate d 1))
  where
    d = V.length a
    normalize v =
        let nrm = sqrt (dot v v) in if nrm == 0 then v else scaleV (1 / nrm) v
    go 0 v = (dot v (matVec a v), v)
    go k v =
        let av = matVec a v
            nrm = sqrt (dot av av)
         in if nrm < 1e-300 then (0, v) else go (k - 1) (scaleV (1 / nrm) av)