linear-massiv-0.1.0.0: src/Numeric/LinearAlgebra/Massiv/Orthogonal/Householder.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
-- |
-- Module : Numeric.LinearAlgebra.Massiv.Orthogonal.Householder
-- Copyright : (c) Nadia Chambers 2026
-- License : BSD-3-Clause
-- Maintainer : nadia.chambers@iohk.io
-- Stability : experimental
--
-- Householder reflections for orthogonal triangularisation.
--
-- This module implements the Householder reflection (also known as a
-- Householder transformation), following Golub & Van Loan, /Matrix
-- Computations/, 4th edition (GVL4), Section 5.1, pp. 236--243.
--
-- As GVL4 states (p. 236): \"The Householder reflection is the most
-- important tool in matrix computations.\" A Householder reflector is a
-- matrix of the form
--
-- \( P = I - \beta v v^T \)
--
-- where \( v \) is the /Householder vector/ and \( \beta = 2 / (v^T v) \).
-- The key property of \( P \) is that it is both symmetric and orthogonal:
--
-- \( P = P^T = P^{-1} \)
--
-- Given an input vector \( x \), the Householder vector \( v \) and scalar
-- \( \beta \) are chosen so that
--
-- \( P x = (I - \beta v v^T) x = \| x \|_2 \, e_1 \)
--
-- where \( e_1 \) is the first standard basis vector. This is the
-- fundamental operation behind Householder QR factorisation (GVL4
-- Algorithm 5.2.1) and many other matrix decompositions.
--
-- __Complexity.__
--
-- * Computing the Householder vector ('householderVector'): \( O(n) \) flops.
-- * Applying a Householder reflection to an \( m \times n \) matrix
-- ('applyHouseholderLeft', 'applyHouseholderRight'): \( O(mn) \) flops.
-- * Forming the explicit reflector matrix ('householderMatrix'): \( O(n^2) \)
-- flops, but this should be avoided in favour of implicit application
-- whenever possible.
module Numeric.LinearAlgebra.Massiv.Orthogonal.Householder
( -- * Householder vector
householderVector
-- * Apply Householder reflection
, applyHouseholderLeft
, applyHouseholderRight
-- * Construct explicit reflector
, householderMatrix
) where
import qualified Data.Massiv.Array as M
import Data.Massiv.Array (Ix1, Ix2(..), Sz(..))
import GHC.TypeNats (KnownNat)
import Numeric.LinearAlgebra.Massiv.Types
import Numeric.LinearAlgebra.Massiv.Internal
-- | Compute a Householder vector (GVL4 Algorithm 5.1.1, p. 236).
--
-- Given a vector \( x \in \mathbb{R}^n \), compute the Householder vector
-- \( v \) and scalar \( \beta \) such that
--
-- \( (I - \beta \, v \, v^T) \, x = \| x \|_2 \, e_1 \)
--
-- where \( e_1 \) is the first standard basis vector. By convention the
-- first component of \( v \) is normalised to \( v_1 = 1 \), which allows
-- it to be stored implicitly in the sub-diagonal part of a matrix during
-- QR factorisation.
--
-- The implementation follows GVL4 Algorithm 5.1.1 exactly, including the
-- careful treatment of the sign of \( x_1 \) to avoid catastrophic
-- cancellation. When \( x \) is already a non-negative multiple of
-- \( e_1 \), the function returns \( \beta = 0 \) (i.e., the identity
-- transformation).
--
-- __Complexity:__ \( O(n) \) flops.
--
-- Returns @(v, beta)@.
householderVector :: forall n r e. (KnownNat n, M.Manifest r e, Floating e, Ord e)
=> Vector n r e -> (Vector n r e, e)
householderVector x =
let nn = dimVal @n
x0 = x !. 0
-- σ = x(2:n)ᵀ · x(2:n)
sigma = foldl' (\acc i -> acc + (x !. i) * (x !. i)) 0 [1..nn-1]
in if sigma == 0 && x0 >= 0
then -- x is already a positive multiple of e1
( makeVector @n @r $ \i -> if i == 0 then 1 else 0
, 0
)
else if sigma == 0
then -- x = -α·e₁
( makeVector @n @r $ \i -> if i == 0 then 1 else 0
, 2
)
else
let mu = sqrt (x0 * x0 + sigma)
v0 = if x0 <= 0 then x0 - mu else -sigma / (x0 + mu)
beta = 2 * v0 * v0 / (sigma + v0 * v0)
v = makeVector @n @r $ \i ->
if i == 0 then 1 else (x !. i) / v0
in (v, beta)
-- | Apply a Householder reflection from the left (GVL4 Section 5.1, p. 236).
--
-- Given a Householder vector \( v \in \mathbb{R}^m \), scalar \( \beta \),
-- and matrix \( A \in \mathbb{R}^{m \times n} \), compute
--
-- \( A \leftarrow (I - \beta \, v \, v^T) \, A = A - \beta \, v \, (A^T v)^T \)
--
-- The computation is performed without forming \( P \) explicitly.
-- Instead, the intermediate vector \( w = \beta \, A^T v \) is computed
-- first, and then the rank-1 update \( A \leftarrow A - v \, w^T \) is
-- applied. This is the standard technique described in GVL4
-- Section 5.1.4 (p. 238).
--
-- __Complexity:__ \( O(mn) \) flops.
applyHouseholderLeft :: forall m n r e. (KnownNat m, KnownNat n, M.Manifest r e, Num e)
=> Vector m r e -> e -> Matrix m n r e -> Matrix m n r e
applyHouseholderLeft v beta a =
let mm = dimVal @m
c = dimVal @n
in makeMatrix @m @n @r $ \i j ->
let -- w = βAᵀv, w(j) = β · Σᵢ v(i)·A(i,j)
wj = beta * foldl' (\acc k -> acc + (v !. k) * (a ! (k, j))) 0 [0..mm-1]
in (a ! (i, j)) - (v !. i) * wj
-- | Apply a Householder reflection from the right (GVL4 Section 5.1, p. 236).
--
-- Given a matrix \( A \in \mathbb{R}^{m \times n} \), Householder vector
-- \( v \in \mathbb{R}^n \), and scalar \( \beta \), compute
--
-- \( A \leftarrow A \, (I - \beta \, v \, v^T) = A - \beta \, (A \, v) \, v^T \)
--
-- As with 'applyHouseholderLeft', the reflector is never formed
-- explicitly. The intermediate vector \( w = \beta \, A \, v \) is
-- computed first, followed by the rank-1 update
-- \( A \leftarrow A - w \, v^T \). See GVL4 Section 5.1.4 (p. 238).
--
-- __Complexity:__ \( O(mn) \) flops.
applyHouseholderRight :: forall m n r e. (KnownNat m, KnownNat n, M.Manifest r e, Num e)
=> Matrix m n r e -> Vector n r e -> e -> Matrix m n r e
applyHouseholderRight a v beta =
let c = dimVal @n
in makeMatrix @m @n @r $ \i j ->
let -- w = β·A·v, w(i) = β · Σⱼ A(i,j)·v(j)
wi = beta * foldl' (\acc k -> acc + (a ! (i, k)) * (v !. k)) 0 [0..c-1]
in (a ! (i, j)) - wi * (v !. j)
-- | Construct the explicit Householder reflector matrix (GVL4 Section 5.1, p. 236).
--
-- Given a Householder vector \( v \in \mathbb{R}^n \) and scalar
-- \( \beta \), form the \( n \times n \) matrix
--
-- \( H = I - \beta \, v \, v^T \)
--
-- The resulting matrix is both symmetric and orthogonal:
-- \( H = H^T = H^{-1} \).
--
-- __Note:__ In most numerical algorithms it is preferable to apply the
-- Householder transformation implicitly via 'applyHouseholderLeft' or
-- 'applyHouseholderRight' rather than forming \( H \) explicitly.
-- Forming the explicit matrix costs \( O(n^2) \) flops and storage, and
-- subsequent multiplication with it costs \( O(n^3) \) rather than the
-- \( O(mn) \) achievable by implicit application.
--
-- __Complexity:__ \( O(n^2) \) flops.
householderMatrix :: forall n r e. (KnownNat n, M.Manifest r e, Num e)
=> Vector n r e -> e -> Matrix n n r e
householderMatrix v beta =
makeMatrix @n @n @r $ \i j ->
let ident = if i == j then 1 else 0
in ident - beta * (v !. i) * (v !. j)