packages feed

linear-massiv-0.1.0.0: src/Numeric/LinearAlgebra/Massiv/Orthogonal/Givens.hs

{-# LANGUAGE AllowAmbiguousTypes #-}

-- |
-- Module      : Numeric.LinearAlgebra.Massiv.Orthogonal.Givens
-- Copyright   : (c) Nadia Chambers 2026
-- License     : BSD-3-Clause
-- Maintainer  : nadia.chambers@iohk.io
-- Stability   : experimental
--
-- Givens rotations for selective zeroing of matrix entries.
--
-- This module implements Givens (plane) rotations following Golub & Van
-- Loan, /Matrix Computations/, 4th edition (GVL4), Section 5.1.8,
-- pp. 240--243.
--
-- A Givens rotation is an orthogonal matrix that operates in a
-- two-dimensional subspace.  Given scalars \( a \) and \( b \), the
-- rotation matrix
--
-- \( G^T = \begin{bmatrix} c & -s \\ s & c \end{bmatrix} \)
--
-- is constructed so that
--
-- \( G^T \begin{bmatrix} a \\ b \end{bmatrix} = \begin{bmatrix} r \\ 0 \end{bmatrix} \)
--
-- where \( r = \sqrt{a^2 + b^2} \).  Our convention follows GVL4
-- Algorithm 5.1.3 (p. 240): \( c = a / r \), \( s = -b / r \).
--
-- Givens rotations are especially useful when only a small number of
-- sub-diagonal entries need to be zeroed (e.g., in Hessenberg or banded
-- matrices), whereas Householder reflections are preferred for zeroing
-- entire sub-columns at once.  Givens-based QR factorisation is the
-- method of choice for tridiagonal and Hessenberg eigenvalue problems
-- (GVL4 Section 5.2.8, p. 255).
--
-- __Complexity.__
--
-- * Computing a Givens rotation ('givensRotation'): \( O(1) \) flops
--   (one square root and a small number of divisions).
-- * Applying a Givens rotation to a row or column pair of an
--   \( m \times n \) matrix ('applyGivensLeft', 'applyGivensRight'):
--   \( O(n) \) or \( O(m) \) flops respectively (one pass over the
--   affected row or column pair).
module Numeric.LinearAlgebra.Massiv.Orthogonal.Givens
  ( -- * Givens rotation
    givensRotation
    -- * Apply Givens rotation
  , applyGivensLeft
  , applyGivensRight
  ) where

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

import Numeric.LinearAlgebra.Massiv.Types
import Numeric.LinearAlgebra.Massiv.Internal

-- | Compute a Givens rotation (GVL4 Algorithm 5.1.3, p. 240).
--
-- Given scalars \( a \) and \( b \), compute cosine \( c \) and sine
-- \( s \) such that
--
-- \( \begin{bmatrix} c & s \\ -s & c \end{bmatrix}^T \begin{bmatrix} a \\ b \end{bmatrix} = \begin{bmatrix} r \\ 0 \end{bmatrix} \)
--
-- where \( r = \sqrt{a^2 + b^2} \).
--
-- The implementation avoids overflow and unnecessary computation by
-- distinguishing three cases:
--
-- * If \( b = 0 \), the rotation is the identity: \( c = 1, s = 0 \).
-- * If \( |b| > |a| \), the tangent \( \tau = -a/b \) is computed first,
--   then \( s = 1 / \sqrt{1 + \tau^2} \) and \( c = s \tau \).
-- * Otherwise, \( \tau = -b/a \), \( c = 1 / \sqrt{1 + \tau^2} \), and
--   \( s = c \tau \).
--
-- This avoids computing the potentially large quantity
-- \( r = \sqrt{a^2 + b^2} \) directly, which could overflow.
--
-- __Complexity:__ \( O(1) \) flops (one square root, a few multiplications
-- and divisions).
--
-- Returns @(c, s)@.
givensRotation :: (Floating e, Ord e) => e -> e -> (e, e)
givensRotation a b
  | b == 0    = (1, 0)
  | abs b > abs a =
      let tau = -a / b
          s = 1 / sqrt (1 + tau * tau)
          c = s * tau
      in (c, s)
  | otherwise =
      let tau = -b / a
          c = 1 / sqrt (1 + tau * tau)
          s = c * tau
      in (c, s)

-- | Apply a Givens rotation from the left to rows @i@ and @k@ of a matrix
-- (GVL4 Section 5.1.9, p. 241).
--
-- Performs the update
--
-- \( A([i,k], :) \leftarrow G^T \, A([i,k], :) \)
--
-- where \( G^T = \begin{bmatrix} c & -s \\ s & c \end{bmatrix} \).
-- Only rows @i@ and @k@ are modified; all other rows are untouched.
-- This is the standard operation used to zero out the \( (k, j) \)
-- entry of a matrix during Givens-based QR factorisation
-- (GVL4 Algorithm 5.2.3, p. 252).
--
-- __Complexity:__ \( O(n) \) flops, where \( n \) is the number of
-- columns.
applyGivensLeft :: forall m n r e. (KnownNat m, KnownNat n, M.Manifest r e, Num e)
                => e    -- ^ c
                -> e    -- ^ s
                -> Int  -- ^ row i
                -> Int  -- ^ row k
                -> Matrix m n r e -> Matrix m n r e
applyGivensLeft c s ri rk a =
  makeMatrix @m @n @r $ \i j ->
    if i == ri then
      c * (a ! (ri, j)) - s * (a ! (rk, j))
    else if i == rk then
      s * (a ! (ri, j)) + c * (a ! (rk, j))
    else
      a ! (i, j)

-- | Apply a Givens rotation from the right to columns @i@ and @k@ of a
-- matrix (GVL4 Section 5.1.9, p. 242).
--
-- Performs the update
--
-- \( A(:, [i,k]) \leftarrow A(:, [i,k]) \, G \)
--
-- where \( G = \begin{bmatrix} c & s \\ -s & c \end{bmatrix} \).
-- Only columns @i@ and @k@ are modified; all other columns are
-- untouched.  Right-multiplication by a Givens rotation is typically
-- used to accumulate the orthogonal factor \( Q \) during QR
-- factorisation (GVL4 Section 5.1.9, p. 242).
--
-- __Complexity:__ \( O(m) \) flops, where \( m \) is the number of
-- rows.
applyGivensRight :: forall m n r e. (KnownNat m, KnownNat n, M.Manifest r e, Num e)
                 => e    -- ^ c
                 -> e    -- ^ s
                 -> Int  -- ^ column i
                 -> Int  -- ^ column k
                 -> Matrix m n r e -> Matrix m n r e
applyGivensRight c s ci ck a =
  makeMatrix @m @n @r $ \i j ->
    if j == ci then
      c * (a ! (i, ci)) - s * (a ! (i, ck))
    else if j == ck then
      s * (a ! (i, ci)) + c * (a ! (i, ck))
    else
      a ! (i, j)