blob: 895590e23de8e477f0c9145ebcd431bf774c0199 [file] [log] [blame]
use matrix::{Matrix, BaseMatrix};
use error::{Error, ErrorKind};
use matrix::decomposition::Decomposition;
use matrix::forward_substitution;
use vector::Vector;
use utils::dot;
use std::vec::*;
use std::any::Any;
use libnum::{Zero, Float};
/// Cholesky decomposition.
///
/// Given a square, symmetric positive definite matrix A,
/// there exists an invertible lower triangular matrix L
/// such that
///
/// A = L L<sup>T</sup>.
///
/// This is called the Cholesky decomposition of A.
/// For not too ill-conditioned A, the computation
/// of the decomposition is very robust, and it takes about
/// half the effort of an LU decomposition with partial pivoting.
///
/// # Applications
/// The Cholesky decomposition can be thought of as a specialized
/// LU decomposition for symmetric positive definite matrices,
/// and so its applications are similar to that of LU.
///
/// The following example shows how to compute the Cholesky
/// decomposition of a given matrix. In this example, we also
/// unpack the decomposition to retrieve the L matrix,
/// but in many practical applications we are not so concerned
/// with the factor itself. Instead, we may wish to
/// solve linear systems or compute the determinant or the
/// inverse of a symmetric positive definite matrix.
/// In this case, see the next subsections.
///
/// ```
/// # #[macro_use] extern crate rulinalg; fn main() {
/// use rulinalg::matrix::decomposition::Cholesky;
///
/// // Need to import Decomposition if we want to unpack
/// use rulinalg::matrix::decomposition::Decomposition;
///
/// let x = matrix![ 1.0, 3.0, 1.0;
/// 3.0, 13.0, 11.0;
/// 1.0, 11.0, 21.0 ];
/// let cholesky = Cholesky::decompose(x)
/// .expect("Matrix is SPD.");
///
/// // Obtain the matrix factor L
/// let l = cholesky.unpack();
///
/// assert_matrix_eq!(l, matrix![1.0, 0.0, 0.0;
/// 3.0, 2.0, 0.0;
/// 1.0, 4.0, 2.0], comp = float);
/// # }
/// ```
///
/// ## Solving linear systems
/// After having decomposed the matrix, one may efficiently
/// solve linear systems for different right-hand sides.
///
/// ```
/// # #[macro_use] extern crate rulinalg; fn main() {
/// # use rulinalg::matrix::decomposition::Cholesky;
/// # let x = matrix![ 1.0, 3.0, 1.0;
/// # 3.0, 13.0, 11.0;
/// # 1.0, 11.0, 21.0 ];
/// # let cholesky = Cholesky::decompose(x).unwrap();
/// let b1 = vector![ 3.0, 2.0, 1.0];
/// let b2 = vector![-2.0, 1.0, 0.0];
/// let y1 = cholesky.solve(b1).expect("Matrix is invertible.");
/// let y2 = cholesky.solve(b2).expect("Matrix is invertible.");
/// assert_vector_eq!(y1, vector![ 23.25, -7.75, 3.0 ]);
/// assert_vector_eq!(y2, vector![-22.25, 7.75, -3.00 ]);
/// # }
/// ```
///
/// ## Computing the inverse of a matrix
///
/// While computing the inverse explicitly is rarely
/// the best solution to any given problem, it is sometimes
/// necessary. In this case, it is easily accessible
/// through the `inverse()` method on `Cholesky`.
///
/// # Computing the determinant of a matrix
///
/// As with LU decomposition, the `Cholesky` decomposition
/// exposes a method `det` for computing the determinant
/// of the decomposed matrix. This is a very cheap operation.
#[derive(Clone, Debug)]
pub struct Cholesky<T> {
l: Matrix<T>
}
impl<T> Cholesky<T> where T: 'static + Float {
/// Computes the Cholesky decomposition A = L L<sup>T</sup>
/// for the given square, symmetric positive definite matrix.
///
/// Note that the implementation cannot reliably and efficiently
/// verify that the matrix truly is symmetric positive definite matrix,
/// so it is the responsibility of the user to make sure that this is
/// the case. In particular, if the input matrix is not SPD,
/// the returned decomposition may not be a valid decomposition
/// for the input matrix.
///
/// # Errors
/// - A diagonal entry is effectively zero to working precision.
/// - A diagonal entry is negative.
///
/// # Panics
///
/// - The matrix must be square.
pub fn decompose(matrix: Matrix<T>) -> Result<Self, Error> {
assert!(matrix.rows() == matrix.cols(),
"Matrix must be square for Cholesky decomposition.");
let n = matrix.rows();
// The implementation here is based on the
// "Gaxpy-Rich Cholesky Factorization"
// from Chapter 4.2.5 in
// Matrix Computations, 4th Edition,
// (Golub and Van Loan).
// We consume the matrix we're given, and overwrite its
// lower diagonal part with the L factor. However,
// we ignore the strictly upper triangular part of the matrix,
// because this saves us a few operations.
// When the decomposition is unpacked, we will completely zero
// the upper triangular part.
let mut a = matrix;
for j in 0 .. n {
if j > 0 {
// This is essentially a GAXPY operation y = y - Bx
// where B is the [j .. n, 0 .. j] submatrix of A,
// x is the [ j, 0 .. j ] submatrix of A,
// and y is the [ j .. n, j ] submatrix of A
for k in j .. n {
let kj_dot = {
let j_row = a.row(j).raw_slice();
let k_row = a.row(k).raw_slice();
dot(&k_row[0 .. j], &j_row[0 .. j])
};
a[[k, j]] = a[[k, j]] - kj_dot;
}
}
let diagonal = a[[j, j]];
if diagonal.abs() < T::epsilon() {
return Err(Error::new(ErrorKind::DecompFailure,
"Matrix is singular to working precision."));
} else if diagonal < T::zero() {
return Err(Error::new(ErrorKind::DecompFailure,
"Diagonal entries of matrix are not all positive."));
}
let divisor = diagonal.sqrt();
for k in j .. n {
a[[k, j]] = a[[k, j]] / divisor;
}
}
Ok(Cholesky {
l: a
})
}
/// Computes the determinant of the decomposed matrix.
///
/// Note that the determinant of an empty matrix is considered
/// to be equal to 1.
pub fn det(&self) -> T {
let l_det = self.l.diag()
.cloned()
.fold(T::one(), |a, b| a * b);
l_det * l_det
}
/// Solves the linear system Ax = b.
///
/// Here A is the decomposed matrix and b is the
/// supplied vector.
///
/// # Errors
/// If the matrix is sufficiently ill-conditioned,
/// it is possible that the solution cannot be obtained.
///
/// # Panics
/// - The supplied right-hand side vector must be
/// dimensionally compatible with the supplied matrix.
pub fn solve(&self, b: Vector<T>) -> Result<Vector<T>, Error> {
assert!(self.l.rows() == b.size(),
"RHS vector and coefficient matrix must be
dimensionally compatible.");
// Solve Ly = b
let y = forward_substitution(&self.l, b)?;
// Solve L^T x = y
transpose_back_substitution(&self.l, y)
}
/// Computes the inverse of the decomposed matrix.
///
/// # Errors
/// If the matrix is sufficiently ill-conditioned,
/// it is possible that the inverse cannot be obtained.
pub fn inverse(&self) -> Result<Matrix<T>, Error> {
let n = self.l.rows();
let mut inv = Matrix::zeros(n, n);
let mut e = Vector::zeros(n);
// Note: this is essentially the same as
// PartialPivLu::inverse(), and consequently
// the data access patterns here can also be
// improved by way of using BLAS-3 calls.
// Please see that function's implementation
// for more details.
// Solve for each column of the inverse matrix
for i in 0 .. n {
e[i] = T::one();
let col = self.solve(e)?;
for j in 0 .. n {
inv[[j, i]] = col[j];
}
e = col.apply(&|_| T::zero());
}
Ok(inv)
}
}
impl<T: Zero> Decomposition for Cholesky<T> {
type Factors = Matrix<T>;
fn unpack(self) -> Matrix<T> {
use internal_utils::nullify_upper_triangular_part;
let mut l = self.l;
nullify_upper_triangular_part(&mut l);
l
}
}
impl<T> Matrix<T>
where T: Any + Float
{
/// Cholesky decomposition
///
/// Returns the cholesky decomposition of a positive definite matrix.
///
/// *NOTE*: This function is deprecated, and will be removed in a
/// future release. Please see
/// [Cholesky](decomposition/struct.Cholesky.html) for its
/// replacement.
///
/// # Examples
///
/// ```
/// # #[macro_use] extern crate rulinalg; fn main() {
/// use rulinalg::matrix::Matrix;
///
/// let m = matrix![1.0, 0.5, 0.5;
/// 0.5, 1.0, 0.5;
/// 0.5, 0.5, 1.0];
///
/// let l = m.cholesky();
/// # }
/// ```
///
/// # Panics
///
/// - The matrix is not square.
///
/// # Failures
///
/// - Matrix is not positive definite.
//#[deprecated]
pub fn cholesky(&self) -> Result<Matrix<T>, Error> {
assert!(self.rows == self.cols,
"Matrix must be square for Cholesky decomposition.");
let mut new_data = Vec::<T>::with_capacity(self.rows() * self.cols());
for i in 0..self.rows() {
for j in 0..self.cols() {
if j > i {
new_data.push(T::zero());
continue;
}
let mut sum = T::zero();
for k in 0..j {
sum = sum + (new_data[i * self.cols() + k] * new_data[j * self.cols() + k]);
}
if j == i {
new_data.push((self[[i, i]] - sum).sqrt());
} else {
let p = (self[[i, j]] - sum) / new_data[j * self.cols + j];
if !p.is_finite() {
return Err(Error::new(ErrorKind::DecompFailure,
"Matrix is not positive definite."));
} else {
}
new_data.push(p);
}
}
}
Ok(Matrix {
rows: self.rows(),
cols: self.cols(),
data: new_data,
})
}
}
/// Solves the square system L^T x = b,
/// where L is lower triangular
fn transpose_back_substitution<T>(l: &Matrix<T>, b: Vector<T>)
-> Result<Vector<T>, Error> where T: Float {
assert!(l.rows() == l.cols(), "Matrix L must be square.");
assert!(l.rows() == b.size(), "L and b must be dimensionally compatible.");
let n = l.rows();
let mut x = b;
for i in (0 .. n).rev() {
let row = l.row(i).raw_slice();
let diagonal = l[[i, i]];
if diagonal.abs() < T::epsilon() {
return Err(Error::new(ErrorKind::DivByZero,
"Matrix L is singular to working precision."));
}
x[i] = x[i] / diagonal;
// Apply the BLAS-1 operation
// y <- y + α x
// where α = - x[i],
// y = x[0 .. i]
// and x = l[i, 0 .. i]
// TODO: Hopefully we'll have a more systematic way
// of applying optimized BLAS-like operations in the future.
// In this case, we should replace this loop with a call
// to the appropriate function.
for j in 0 .. i {
x[j] = x[j] - x[i] * row[j];
}
}
Ok(x)
}
#[cfg(test)]
mod tests {
use matrix::Matrix;
use matrix::decomposition::Decomposition;
use vector::Vector;
use super::Cholesky;
use super::transpose_back_substitution;
use quickcheck::TestResult;
#[test]
#[should_panic]
#[allow(deprecated)]
fn test_non_square_cholesky() {
let a = Matrix::<f64>::ones(2, 3);
let _ = a.cholesky();
}
#[test]
fn cholesky_unpack_empty() {
let x: Matrix<f64> = matrix![];
let l = Cholesky::decompose(x.clone())
.unwrap()
.unpack();
assert_matrix_eq!(l, x);
}
#[test]
fn cholesky_unpack_1x1() {
let x = matrix![ 4.0 ];
let expected = matrix![ 2.0 ];
let l = Cholesky::decompose(x)
.unwrap()
.unpack();
assert_matrix_eq!(l, expected, comp = float);
}
#[test]
fn cholesky_unpack_2x2() {
{
let x = matrix![ 9.0, -6.0;
-6.0, 20.0];
let expected = matrix![ 3.0, 0.0;
-2.0, 4.0];
let l = Cholesky::decompose(x)
.unwrap()
.unpack();
assert_matrix_eq!(l, expected, comp = float);
}
}
#[test]
fn cholesky_singular_fails() {
{
let x = matrix![0.0];
assert!(Cholesky::decompose(x).is_err());
}
{
let x = matrix![0.0, 0.0;
0.0, 1.0];
assert!(Cholesky::decompose(x).is_err());
}
{
let x = matrix![1.0, 0.0;
0.0, 0.0];
assert!(Cholesky::decompose(x).is_err());
}
{
let x = matrix![1.0, 3.0, 5.0;
3.0, 9.0, 15.0;
5.0, 15.0, 65.0];
assert!(Cholesky::decompose(x).is_err());
}
}
#[test]
fn cholesky_det_empty() {
let x: Matrix<f64> = matrix![];
let cholesky = Cholesky::decompose(x).unwrap();
assert_eq!(cholesky.det(), 1.0);
}
#[test]
fn cholesky_det() {
{
let x = matrix![1.0];
let cholesky = Cholesky::decompose(x).unwrap();
assert_scalar_eq!(cholesky.det(), 1.0, comp = float);
}
{
let x = matrix![1.0, 3.0, 5.0;
3.0, 18.0, 33.0;
5.0, 33.0, 65.0];
let cholesky = Cholesky::decompose(x).unwrap();
assert_scalar_eq!(cholesky.det(), 36.0, comp = float);
}
}
#[test]
fn cholesky_solve_examples() {
{
let a: Matrix<f64> = matrix![];
let b: Vector<f64> = vector![];
let expected: Vector<f64> = vector![];
let cholesky = Cholesky::decompose(a).unwrap();
let x = cholesky.solve(b).unwrap();
assert_eq!(x, expected);
}
{
let a = matrix![ 1.0 ];
let b = vector![ 4.0 ];
let expected = vector![ 4.0 ];
let cholesky = Cholesky::decompose(a).unwrap();
let x = cholesky.solve(b).unwrap();
assert_vector_eq!(x, expected, comp = float);
}
{
let a = matrix![ 4.0, 6.0;
6.0, 25.0];
let b = vector![ 2.0, 4.0];
let expected = vector![ 0.40625, 0.0625 ];
let cholesky = Cholesky::decompose(a).unwrap();
let x = cholesky.solve(b).unwrap();
assert_vector_eq!(x, expected, comp = float);
}
}
#[test]
fn cholesky_inverse_examples() {
{
let a: Matrix<f64> = matrix![];
let expected: Matrix<f64> = matrix![];
let cholesky = Cholesky::decompose(a).unwrap();
assert_eq!(cholesky.inverse().unwrap(), expected);
}
{
let a = matrix![ 2.0 ];
let expected = matrix![ 0.5 ];
let cholesky = Cholesky::decompose(a).unwrap();
assert_matrix_eq!(cholesky.inverse().unwrap(), expected,
comp = float);
}
{
let a = matrix![ 4.0, 6.0;
6.0, 25.0];
let expected = matrix![ 0.390625, -0.09375;
-0.093750 , 0.06250];
let cholesky = Cholesky::decompose(a).unwrap();
assert_matrix_eq!(cholesky.inverse().unwrap(), expected,
comp = float);
}
{
let a = matrix![ 9.0, 6.0, 3.0;
6.0, 20.0, 10.0;
3.0, 10.0, 14.0];
let expected = matrix![0.1388888888888889, -0.0416666666666667, 0.0 ;
-0.0416666666666667, 0.0902777777777778, -0.0555555555555556;
0.0, -0.0555555555555556, 0.1111111111111111];
let cholesky = Cholesky::decompose(a).unwrap();
assert_matrix_eq!(cholesky.inverse().unwrap(), expected,
comp = float);
}
}
quickcheck! {
fn property_cholesky_of_identity_is_identity(n: usize) -> TestResult {
if n > 30 {
return TestResult::discard();
}
let x = Matrix::<f64>::identity(n);
let l = Cholesky::decompose(x.clone()).map(|c| c.unpack());
match l {
Ok(l) => {
assert_matrix_eq!(l, x, comp = float);
TestResult::passed()
},
_ => TestResult::failed()
}
}
}
#[test]
fn transpose_back_substitution_examples() {
{
let l: Matrix<f64> = matrix![];
let b: Vector<f64> = vector![];
let expected: Vector<f64> = vector![];
let x = transpose_back_substitution(&l, b).unwrap();
assert_vector_eq!(x, expected);
}
{
let l = matrix![2.0];
let b = vector![2.0];
let expected = vector![1.0];
let x = transpose_back_substitution(&l, b).unwrap();
assert_vector_eq!(x, expected, comp = float);
}
{
let l = matrix![2.0, 0.0;
3.0, 4.0];
let b = vector![2.0, 1.0];
let expected = vector![0.625, 0.25 ];
let x = transpose_back_substitution(&l, b).unwrap();
assert_vector_eq!(x, expected, comp = float);
}
{
let l = matrix![ 2.0, 0.0, 0.0;
5.0, -1.0, 0.0;
-2.0, 0.0, 1.0];
let b = vector![-1.0, 2.0, 3.0];
let expected = vector![ 7.5, -2.0, 3.0 ];
let x = transpose_back_substitution(&l, b).unwrap();
assert_vector_eq!(x, expected, comp = float);
}
}
}