Skip to content

Numerical computation in native Haskell

License

Notifications You must be signed in to change notification settings

ocramz/sparse-linear-algebra

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

sparse-linear-algebra

Numerical computation in native Haskell

CI-library CI-matrix-factorizations Hackage
sparse-linear-algebra sparse-linear-algebra

This library provides common numerical analysis functionality, without requiring any external bindings. It aims to serve as an experimental platform for scientific computation in a purely functional setting.

Project status

January 2026: Re-enabled BiCGSTAB (Biconjugate Gradient Stabilized) solver with comprehensive tests. BiCGSTAB is more numerically stable than CGS and is the recommended choice for non-Hermitian systems. Also fixed CGS (Conjugate Gradient Squared) numerical convergence issues. Both solvers now properly handle early termination and use appropriate tolerances for iterative methods.

Note on property tests: Property-based tests for iterative solvers (BiCGSTAB, CGS) guard against degenerate cases (tiny systems, nearly zero RHS/solution, very sparse matrices) which can cause flaky behavior with randomly generated SPD matrices. This is expected behavior and reflects the numerical limitations of iterative methods on ill-conditioned systems.

December 2025: The project sat unmaintained for a few years but I'm not comfortable with leaving projects incomplete. There are some hard design problems under the hood (inefficient data representation with lots of memory copies, incorrect algorithms, numerical instability) that make progress a slog.

Mar 14, 2018: The core linear algebra operations work, but there are still a few (documented) bugs such as in the matrix factorizations department. Complex number support is still incomplete, so the users are advised to not rely on that for the time being. The issues related to Complex number handling are tracked in #51, #12, #30.

Refer to the Changelog for detailed status updates.

Contents

  • "BLAS" levels 1 to 3 : vector, matrix operations including matrix products etc.

  • Iterative linear solvers (<\>)

    • Generalized Minimal Residual (GMRES) (non-Hermitian systems) 🚧

    • BiConjugate Gradient (BCG) 🚧

    • Conjugate Gradient Squared (CGS) ✅

      • Suitable for non-Hermitian systems
      • Fast convergence for well-conditioned problems
      • May exhibit irregular convergence on ill-conditioned systems
      • Use cgsInit and cgsStep for manual iteration control
      • Supports debug tracing with cgsStepDebug
    • BiConjugate Gradient Stabilized (BiCGSTAB) ✅

      • Suitable for non-Hermitian systems
      • More numerically stable than CGS
      • Recommended for ill-conditioned problems
      • Smoother convergence behavior than CGS
      • Use bicgsInit and bicgstabStep for manual iteration control
      • Access via linSolve0 interface with BICGSTAB_ method
    • Transpose-Free Quasi-Minimal Residual (TFQMR) 🚧

    • Moore-Penrose pseudoinverse (pinv) (rectangular systems)

  • Direct linear solvers

    • LU-based (luSolve); forward and backward substitution (triLowerSolve, triUpperSolve)
  • Matrix factorization algorithms

    • QR (qr) 🚧
    • LU (lu) 🚧
    • Cholesky (chol) 🚧
    • Arnoldi iteration (arnoldi) 🚧
    • Golub-Kahan-Lanczos bidiagonalization (gklBidiag) 🚧
    • Singular value decomposition (SVD) 🚧
  • Eigenvalue algorithms

    • QR (eigsQR) 🚧
    • QR-Arnoldi (eigsArnoldi) 🚧
    • Rayleigh quotient iteration (eigRayleigh) 🚧
  • Utilities : Vector and matrix norms, matrix condition number, Givens rotation, Householder reflection

  • Predicates : Matrix orthogonality test (A^T A ~= I)


Examples

The module Numeric.LinearAlgebra.Sparse contains the user interface.

Creation of sparse data

The fromListSM function creates a sparse matrix from a collection of its entries in (row, column, value) format. This is its type signature:

fromListSM :: Foldable t => (Int, Int) -> t (IxRow, IxCol, a) -> SpMatrix a

and, in case you have a running GHCi session (the terminal is denoted from now on by λ>), you can try something like this:

λ> amat = fromListSM (3,3) [(0,0,2),(1,0,4),(1,1,3),(1,2,2),(2,2,5)] :: SpMatrix Double

Similarly, fromListSV is used to create sparse vectors:

fromListSV :: Int -> [(Int, a)] -> SpVector a

Alternatively, the user can copy the contents of a list to a (dense) SpVector using

fromListDenseSV :: Int -> [a] -> SpVector a

Displaying sparse data

Both sparse vectors and matrices can be pretty-printed using prd:

λ> prd amat

( 3 rows, 3 columns ) , 5 NZ ( density 55.556 % )

2.00   , _      , _      
4.00   , 3.00   , 2.00   
_      , _      , 5.00       

Note (sparse storage): sparse data should only contain non-zero entries not to waste memory and computation.

Note (approximate output): prd rounds the results to two significant digits, and switches to scientific notation for large or small values. Moreover, values which are indistinguishable from 0 (see the Numeric.Eps module) are printed as _.

Matrix factorizations, matrix product

There are a few common matrix factorizations available; in the following example we compute the LU factorization of matrix amat and verify it with the matrix-matrix product ## of its factors :

λ> (l, u) <- lu amat
λ> prd $ l ## u

( 3 rows, 3 columns ) , 9 NZ ( density 100.000 % )

2.00   , _      , _      
4.00   , 3.00   , 2.00   
_      , _      , 5.00       

Notice that the result is dense, i.e. certain entries are numerically zero but have been inserted into the result along with all the others (thus taking up memory!). To preserve sparsity, we can use a sparsifying matrix-matrix product #~#, which filters out all the elements x for which |x| <= eps, where eps (defined in Numeric.Eps) depends on the numerical type used (e.g. it is 10^-6 for Floats and 10^-12 for Doubles).

λ> prd $ l #~# u

( 3 rows, 3 columns ) , 5 NZ ( density 55.556 % )

2.00   , _      , _      
4.00   , 3.00   , 2.00   
_      , _      , 5.00 

A matrix is transposed using the transpose function.

Sometimes we need to compute matrix-matrix transpose products, which is why the library offers the infix operators #^# (i.e. matrix transpose * matrix) and ##^ (matrix * matrix transpose):

λ> amat' = amat #^# amat
λ> prd amat'

( 3 rows, 3 columns ) , 9 NZ ( density 100.000 % )

20.00  , 12.00  , 8.00   
12.00  , 9.00   , 6.00   
8.00   , 6.00   , 29.00      


λ> lc <- chol amat'
λ> prd $ lc ##^ lc

( 3 rows, 3 columns ) , 9 NZ ( density 100.000 % )

20.00  , 12.00  , 8.00   
12.00  , 9.00   , 10.80  
8.00   , 10.80  , 29.00      

In the last example we have also shown the Cholesky decomposition (M = L L^T where L is a lower-triangular matrix), which is only defined for symmetric positive-definite matrices.

Linear systems

Large sparse linear systems are best solved with iterative methods. sparse-linear-algebra provides a selection of these via the <\> (inspired by Matlab's "backslash" function. Currently this method uses GMRES as default) :

λ> b = fromListDenseSV 3 [3,2,5] :: SpVector Double
λ> x <- amat <\> b
λ> prd x

( 3 elements ) ,  3 NZ ( density 100.000 % )

1.50   , -2.00  , 1.00      

The result can be verified by computing the matrix-vector action amat #> x, which should (ideally) be very close to the right-hand side b :

λ> prd $ amat #> x

( 3 elements ) ,  3 NZ ( density 100.000 % )

3.00   , 2.00   , 5.00       

Using CGS (Conjugate Gradient Squared)

For more control over the iterative solution process, you can use the CGS solver directly:

λ> let x0 = fromListSV 3 []  -- initial guess (zero vector)
λ> let rhat = b ^-^ (amat #> x0)  -- shadow residual
λ> let initState = cgsInit amat b x0
λ> let finalState = iterate (cgsStep amat rhat) initState !! 20  -- 20 iterations
λ> prd $ _x finalState

( 3 elements ) ,  3 NZ ( density 100.000 % )

1.50   , -2.00  , 1.00

Note: CGS can converge quickly but may become numerically unstable if iterations continue past convergence. For production use, implement convergence monitoring and early termination when the residual norm is sufficiently small.

Using BiCGSTAB (Biconjugate Gradient Stabilized)

BiCGSTAB is more stable than CGS and is recommended for difficult problems:

λ> let x0 = fromListSV 3 []  -- initial guess (zero vector)
λ> let r0hat = b ^-^ (amat #> x0)  -- fixed shadow residual
λ> let initState = bicgsInit amat b x0
λ> let finalState = iterate (bicgstabStep amat r0hat) initState !! 20
λ> prd $ _xBicgstab finalState

( 3 elements ) ,  3 NZ ( density 100.000 % )

1.50   , -2.00  , 1.00

BiCGSTAB typically exhibits smoother convergence than CGS and is less sensitive to numerical instability. It's particularly effective for ill-conditioned non-Hermitian systems.

You can also use the linSolve0 interface to access BiCGSTAB:

λ> x <- linSolve0 BICGSTAB_ amat b x0
λ> prd x

( 3 elements ) ,  3 NZ ( density 100.000 % )

1.50   , -2.00  , 1.00

Direct solvers

The library also provides a forward-backward substitution solver (luSolve) based on a triangular factorization of the system matrix (usually LU). This should be the preferred for solving smaller, dense systems. Using the LU factors defined previously we can cross-verify the two solution methods:

λ> x' <- luSolve l u b
λ> prd x'

( 3 elements ) ,  3 NZ ( density 100.000 % )

1.50   , -2.00  , 1.00     

License

GPL3, see LICENSE

Credits

Inspired by

References

[1] Y. Saad, Iterative Methods for Sparse Linear Systems, 2nd ed., 2000

[2] G.H. Golub and C.F. Van Loan, Matrix Computations, 3rd ed., 1996

[3] T.A. Davis, Direct Methods for Sparse Linear Systems, 2006

[4] L.N. Trefethen, D. Bau, Numerical Linear Algebra, SIAM, 1997

[5] W.H. Press, S.A. Teukolsky, W.T. Vetterling, B.P. Flannery, Numerical Recipes in Fortran 77, 2nd ed., 1992

[6] M. M. T. Chakravarty, et al., Accelerating Haskell array codes with multicore GPUs - DAMP'11

[7] accelerate