Files
gonum/mat/cholesky.go
2017-06-20 16:29:35 +09:30

458 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright ©2013 The gonum Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Based on the CholeskyDecomposition class from Jama 1.0.3.
package mat
import (
"math"
"gonum.org/v1/gonum/blas"
"gonum.org/v1/gonum/blas/blas64"
"gonum.org/v1/gonum/lapack/lapack64"
)
const (
badTriangle = "mat: invalid triangle"
badCholesky = "mat: invalid Cholesky factorization"
)
// Cholesky is a type for creating and using the Cholesky factorization of a
// symmetric positive definite matrix.
//
// Cholesky methods may only be called on a value that has been successfully
// initialized by a call to Factorize that has returned true. Calls to methods
// of an unsuccessful Cholesky factorization will panic.
type Cholesky struct {
// The chol pointer must never be retained as a pointer outside the Cholesky
// struct, either by returning chol outside the struct or by setting it to
// a pointer coming from outside. The same prohibition applies to the data
// slice within chol.
chol *TriDense
cond float64
}
// updateCond updates the condition number of the Cholesky decomposition. If
// norm > 0, then that norm is used as the norm of the original matrix A, otherwise
// the norm is estimated from the decomposition.
func (c *Cholesky) updateCond(norm float64) {
n := c.chol.mat.N
work := getFloats(3*n, false)
defer putFloats(work)
if norm < 0 {
// This is an approximation. By the definition of a norm, ||AB|| <= ||A|| ||B||.
// Here, A = U^T * U.
// The condition number is ||A|| || A^-1||, so this will underestimate
// the condition number somewhat.
// The norm of the original factorized matrix cannot be stored because of
// update possibilities.
unorm := lapack64.Lantr(CondNorm, c.chol.mat, work)
lnorm := lapack64.Lantr(CondNormTrans, c.chol.mat, work)
norm = unorm * lnorm
}
sym := c.chol.asSymBlas()
iwork := getInts(n, false)
v := lapack64.Pocon(sym, norm, work, iwork)
putInts(iwork)
c.cond = 1 / v
}
// Factorize calculates the Cholesky decomposition of the matrix A and returns
// whether the matrix is positive definite. If Factorize returns false, the
// factorization must not be used.
func (c *Cholesky) Factorize(a Symmetric) (ok bool) {
n := a.Symmetric()
if c.isZero() {
c.chol = NewTriDense(n, Upper, nil)
} else {
c.chol = NewTriDense(n, Upper, use(c.chol.mat.Data, n*n))
}
copySymIntoTriangle(c.chol, a)
sym := c.chol.asSymBlas()
work := getFloats(c.chol.mat.N, false)
norm := lapack64.Lansy(CondNorm, sym, work)
putFloats(work)
_, ok = lapack64.Potrf(sym)
if ok {
c.updateCond(norm)
} else {
c.Reset()
}
return ok
}
// Reset resets the factorization so that it can be reused as the receiver of a
// dimensionally restricted operation.
func (c *Cholesky) Reset() {
if !c.isZero() {
c.chol.Reset()
}
c.cond = math.Inf(1)
}
// SetFromU sets the Cholesky decomposition from the given triangular matrix.
// SetFromU panics if t is not upper triangular. Note that t is copied into,
// not stored inside, the receiver.
func (c *Cholesky) SetFromU(t *TriDense) {
n, kind := t.Triangle()
if kind != Upper {
panic("cholesky: matrix must be upper triangular")
}
if c.isZero() {
c.chol = NewTriDense(n, Upper, nil)
} else {
c.chol = NewTriDense(n, Upper, use(c.chol.mat.Data, n*n))
}
c.chol.Copy(t)
c.updateCond(-1)
}
// Clone makes a copy of the input Cholesky into the receiver, overwriting the
// previous value of the receiver. Clone does not place any restrictions on receiver
// shape. Clone panics if the input Cholesky is not the result of a valid decomposition.
func (c *Cholesky) Clone(chol *Cholesky) {
if !chol.valid() {
panic(badCholesky)
}
n := chol.Size()
if c.isZero() {
c.chol = NewTriDense(n, Upper, nil)
} else {
c.chol = NewTriDense(n, Upper, use(c.chol.mat.Data, n*n))
}
c.chol.Copy(chol.chol)
c.cond = chol.cond
}
// Size returns the dimension of the factorized matrix.
func (c *Cholesky) Size() int {
if !c.valid() {
panic(badCholesky)
}
return c.chol.mat.N
}
// Det returns the determinant of the matrix that has been factorized.
func (c *Cholesky) Det() float64 {
if !c.valid() {
panic(badCholesky)
}
return math.Exp(c.LogDet())
}
// LogDet returns the log of the determinant of the matrix that has been factorized.
func (c *Cholesky) LogDet() float64 {
if !c.valid() {
panic(badCholesky)
}
var det float64
for i := 0; i < c.chol.mat.N; i++ {
det += 2 * math.Log(c.chol.mat.Data[i*c.chol.mat.Stride+i])
}
return det
}
// SolveCholesky finds the matrix m that solves A * m = b where A is represented
// by the Cholesky decomposition, placing the result in the receiver.
func (m *Dense) SolveCholesky(chol *Cholesky, b Matrix) error {
if !chol.valid() {
panic(badCholesky)
}
n := chol.chol.mat.N
bm, bn := b.Dims()
if n != bm {
panic(ErrShape)
}
m.reuseAs(bm, bn)
if b != m {
m.Copy(b)
}
blas64.Trsm(blas.Left, blas.Trans, 1, chol.chol.mat, m.mat)
blas64.Trsm(blas.Left, blas.NoTrans, 1, chol.chol.mat, m.mat)
if chol.cond > ConditionTolerance {
return Condition(chol.cond)
}
return nil
}
// TODO(kortschak): Export this as SolveTwoChol.
// solveTwoChol finds the matrix m that solves A * m = B where A and B are represented
// by their Cholesky decompositions a and b, placing the result in the receiver.
func (m *Dense) solveTwoChol(a, b *Cholesky) error {
if !a.valid() || !b.valid() {
panic(badCholesky)
}
bn := b.chol.mat.N
if a.chol.mat.N != bn {
panic(ErrShape)
}
m.reuseAsZeroed(bn, bn)
m.Copy(b.chol.T())
blas64.Trsm(blas.Left, blas.Trans, 1, a.chol.mat, m.mat)
blas64.Trsm(blas.Left, blas.NoTrans, 1, a.chol.mat, m.mat)
blas64.Trmm(blas.Right, blas.NoTrans, 1, b.chol.mat, m.mat)
if a.cond > ConditionTolerance {
return Condition(a.cond)
}
return nil
}
// SolveCholeskyVec finds the vector v that solves A * v = b where A is represented
// by the Cholesky decomposition, placing the result in the receiver.
func (v *Vector) SolveCholeskyVec(chol *Cholesky, b *Vector) error {
if !chol.valid() {
panic(badCholesky)
}
n := chol.chol.mat.N
vn := b.Len()
if vn != n {
panic(ErrShape)
}
if v != b {
v.checkOverlap(b.mat)
}
v.reuseAs(n)
if v != b {
v.CopyVec(b)
}
blas64.Trsv(blas.Trans, chol.chol.mat, v.mat)
blas64.Trsv(blas.NoTrans, chol.chol.mat, v.mat)
if chol.cond > ConditionTolerance {
return Condition(chol.cond)
}
return nil
}
// UFromCholesky extracts the n×n upper triangular matrix U from a Cholesky
// decomposition
// A = U^T * U.
func (t *TriDense) UFromCholesky(chol *Cholesky) {
if !chol.valid() {
panic(badCholesky)
}
n := chol.chol.mat.N
t.reuseAs(n, Upper)
t.Copy(chol.chol)
}
// LFromCholesky extracts the n×n lower triangular matrix L from a Cholesky
// decomposition
// A = L * L^T.
func (t *TriDense) LFromCholesky(chol *Cholesky) {
if !chol.valid() {
panic(badCholesky)
}
n := chol.chol.mat.N
t.reuseAs(n, Lower)
t.Copy(chol.chol.TTri())
}
// FromCholesky reconstructs the original positive definite matrix given its
// Cholesky decomposition.
func (s *SymDense) FromCholesky(chol *Cholesky) {
if !chol.valid() {
panic(badCholesky)
}
n := chol.chol.mat.N
s.reuseAs(n)
s.SymOuterK(1, chol.chol.T())
}
// InverseCholesky computes the inverse of the matrix represented by its Cholesky
// factorization and stores the result into the receiver. If the factorized
// matrix is ill-conditioned, a Condition error will be returned.
// Note that matrix inversion is numerically unstable, and should generally be
// avoided where possible, for example by using the Solve routines.
func (s *SymDense) InverseCholesky(chol *Cholesky) error {
if !chol.valid() {
panic(badCholesky)
}
// TODO(btracey): Replace this code with a direct call to Dpotri when it
// is available.
s.reuseAs(chol.chol.mat.N)
// If:
// chol(A) = U^T * U
// Then:
// chol(A^-1) = S * S^T
// where S = U^-1
var t TriDense
err := t.InverseTri(chol.chol)
s.SymOuterK(1, &t)
return err
}
// SymRankOne performs a rank-1 update of the original matrix A and refactorizes
// its Cholesky factorization, storing the result into the receiver. That is, if
// in the original Cholesky factorization
// U^T * U = A,
// in the updated factorization
// U'^T * U' = A + alpha * x * x^T = A'.
//
// Note that when alpha is negative, the updating problem may be ill-conditioned
// and the results may be inaccurate, or the updated matrix A' may not be
// positive definite and not have a Cholesky factorization. SymRankOne returns
// whether the updated matrix A' is positive definite.
//
// SymRankOne updates a Cholesky factorization in O(n²) time. The Cholesky
// factorization computation from scratch is O(n³).
func (c *Cholesky) SymRankOne(orig *Cholesky, alpha float64, x *Vector) (ok bool) {
if !orig.valid() {
panic(badCholesky)
}
n := orig.Size()
if x.Len() != n {
panic(ErrShape)
}
if orig != c {
if c.isZero() {
c.chol = NewTriDense(n, Upper, nil)
} else if c.chol.mat.N != n {
panic(ErrShape)
}
c.chol.Copy(orig.chol)
}
if alpha == 0 {
return true
}
// Algorithms for updating and downdating the Cholesky factorization are
// described, for example, in
// - J. J. Dongarra, J. R. Bunch, C. B. Moler, G. W. Stewart: LINPACK
// Users' Guide. SIAM (1979), pages 10.10--10.14
// or
// - P. E. Gill, G. H. Golub, W. Murray, and M. A. Saunders: Methods for
// modifying matrix factorizations. Mathematics of Computation 28(126)
// (1974), Method C3 on page 521
//
// The implementation is based on LINPACK code
// http://www.netlib.org/linpack/dchud.f
// http://www.netlib.org/linpack/dchdd.f
// and
// https://icl.cs.utk.edu/lapack-forum/viewtopic.php?f=2&t=2646
//
// According to http://icl.cs.utk.edu/lapack-forum/archives/lapack/msg00301.html
// LINPACK is released under BSD license.
//
// See also:
// - M. A. Saunders: Large-scale Linear Programming Using the Cholesky
// Factorization. Technical Report Stanford University (1972)
// http://i.stanford.edu/pub/cstr/reports/cs/tr/72/252/CS-TR-72-252.pdf
// - Matthias Seeger: Low rank updates for the Cholesky decomposition.
// EPFL Technical Report 161468 (2004)
// http://infoscience.epfl.ch/record/161468
work := getFloats(n, false)
defer putFloats(work)
blas64.Copy(n, x.RawVector(), blas64.Vector{1, work})
if alpha > 0 {
// Compute rank-1 update.
if alpha != 1 {
blas64.Scal(n, math.Sqrt(alpha), blas64.Vector{1, work})
}
umat := c.chol.mat
stride := umat.Stride
for i := 0; i < n; i++ {
// Compute parameters of the Givens matrix that zeroes
// the i-th element of x.
c, s, r, _ := blas64.Rotg(umat.Data[i*stride+i], work[i])
if r < 0 {
// Multiply by -1 to have positive diagonal
// elemnts.
r *= -1
c *= -1
s *= -1
}
umat.Data[i*stride+i] = r
if i < n-1 {
// Multiply the extended factorization matrix by
// the Givens matrix from the left. Only
// the i-th row and x are modified.
blas64.Rot(n-i-1,
blas64.Vector{1, umat.Data[i*stride+i+1 : i*stride+n]},
blas64.Vector{1, work[i+1 : n]},
c, s)
}
}
c.updateCond(-1)
return true
}
// Compute rank-1 downdate.
alpha = math.Sqrt(-alpha)
if alpha != 1 {
blas64.Scal(n, alpha, blas64.Vector{1, work})
}
// Solve U^T * p = x storing the result into work.
ok = lapack64.Trtrs(blas.Trans, c.chol.RawTriangular(), blas64.General{
Rows: n,
Cols: 1,
Stride: 1,
Data: work,
})
if !ok {
// The original matrix is singular. Should not happen, because
// the factorization is valid.
panic(badCholesky)
}
norm := blas64.Nrm2(n, blas64.Vector{1, work})
if norm >= 1 {
// The updated matrix is not positive definite.
return false
}
norm = math.Sqrt((1 + norm) * (1 - norm))
cos := getFloats(n, false)
defer putFloats(cos)
sin := getFloats(n, false)
defer putFloats(sin)
for i := n - 1; i >= 0; i-- {
// Compute parameters of Givens matrices that zero elements of p
// backwards.
cos[i], sin[i], norm, _ = blas64.Rotg(norm, work[i])
if norm < 0 {
norm *= -1
cos[i] *= -1
sin[i] *= -1
}
}
umat := c.chol.mat
stride := umat.Stride
for i := n - 1; i >= 0; i-- {
// Apply Givens matrices to U.
// TODO(vladimir-ch): Use workspace to avoid modifying the
// receiver in case an invalid factorization is created.
blas64.Rot(n-i, blas64.Vector{1, work[i:n]}, blas64.Vector{1, umat.Data[i*stride+i : i*stride+n]}, cos[i], sin[i])
if umat.Data[i*stride+i] == 0 {
// The matrix is singular (may rarely happen due to
// floating-point effects?).
ok = false
} else if umat.Data[i*stride+i] < 0 {
// Diagonal elements should be positive. If it happens
// that on the i-th row the diagonal is negative,
// multiply U from the left by an identity matrix that
// has -1 on the i-th row.
blas64.Scal(n-i, -1, blas64.Vector{1, umat.Data[i*stride+i : i*stride+n]})
}
}
if ok {
c.updateCond(-1)
} else {
c.Reset()
}
return ok
}
func (c *Cholesky) isZero() bool {
return c.chol == nil
}
func (c *Cholesky) valid() bool {
return !c.isZero() && !c.chol.isZero()
}