1 // Copyright ©2013 The Gonum Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
10 "gonum.org/v1/gonum/blas"
11 "gonum.org/v1/gonum/blas/blas64"
12 "gonum.org/v1/gonum/lapack/lapack64"
16 badTriangle = "mat: invalid triangle"
17 badCholesky = "mat: invalid Cholesky factorization"
20 // Cholesky is a type for creating and using the Cholesky factorization of a
21 // symmetric positive definite matrix.
23 // Cholesky methods may only be called on a value that has been successfully
24 // initialized by a call to Factorize that has returned true. Calls to methods
25 // of an unsuccessful Cholesky factorization will panic.
26 type Cholesky struct {
27 // The chol pointer must never be retained as a pointer outside the Cholesky
28 // struct, either by returning chol outside the struct or by setting it to
29 // a pointer coming from outside. The same prohibition applies to the data
35 // updateCond updates the condition number of the Cholesky decomposition. If
36 // norm > 0, then that norm is used as the norm of the original matrix A, otherwise
37 // the norm is estimated from the decomposition.
38 func (c *Cholesky) updateCond(norm float64) {
40 work := getFloats(3*n, false)
43 // This is an approximation. By the definition of a norm,
45 // Since A = U^T*U, we get for the condition number κ that
46 // κ(A) := |A| |A^-1| = |U^T*U| |A^-1| <= |U^T| |U| |A^-1|,
47 // so this will overestimate the condition number somewhat.
48 // The norm of the original factorized matrix cannot be stored
49 // because of update possibilities.
50 unorm := lapack64.Lantr(CondNorm, c.chol.mat, work)
51 lnorm := lapack64.Lantr(CondNormTrans, c.chol.mat, work)
54 sym := c.chol.asSymBlas()
55 iwork := getInts(n, false)
56 v := lapack64.Pocon(sym, norm, work, iwork)
61 // Cond returns the condition number of the factorized matrix.
62 func (c *Cholesky) Cond() float64 {
66 // Factorize calculates the Cholesky decomposition of the matrix A and returns
67 // whether the matrix is positive definite. If Factorize returns false, the
68 // factorization must not be used.
69 func (c *Cholesky) Factorize(a Symmetric) (ok bool) {
72 c.chol = NewTriDense(n, Upper, nil)
74 c.chol = NewTriDense(n, Upper, use(c.chol.mat.Data, n*n))
76 copySymIntoTriangle(c.chol, a)
78 sym := c.chol.asSymBlas()
79 work := getFloats(c.chol.mat.N, false)
80 norm := lapack64.Lansy(CondNorm, sym, work)
82 _, ok = lapack64.Potrf(sym)
91 // Reset resets the factorization so that it can be reused as the receiver of a
92 // dimensionally restricted operation.
93 func (c *Cholesky) Reset() {
100 // SetFromU sets the Cholesky decomposition from the given triangular matrix.
101 // SetFromU panics if t is not upper triangular. Note that t is copied into,
102 // not stored inside, the receiver.
103 func (c *Cholesky) SetFromU(t *TriDense) {
104 n, kind := t.Triangle()
106 panic("cholesky: matrix must be upper triangular")
109 c.chol = NewTriDense(n, Upper, nil)
111 c.chol = NewTriDense(n, Upper, use(c.chol.mat.Data, n*n))
117 // Clone makes a copy of the input Cholesky into the receiver, overwriting the
118 // previous value of the receiver. Clone does not place any restrictions on receiver
119 // shape. Clone panics if the input Cholesky is not the result of a valid decomposition.
120 func (c *Cholesky) Clone(chol *Cholesky) {
126 c.chol = NewTriDense(n, Upper, nil)
128 c.chol = NewTriDense(n, Upper, use(c.chol.mat.Data, n*n))
130 c.chol.Copy(chol.chol)
134 // Size returns the dimension of the factorized matrix.
135 func (c *Cholesky) Size() int {
142 // Det returns the determinant of the matrix that has been factorized.
143 func (c *Cholesky) Det() float64 {
147 return math.Exp(c.LogDet())
150 // LogDet returns the log of the determinant of the matrix that has been factorized.
151 func (c *Cholesky) LogDet() float64 {
156 for i := 0; i < c.chol.mat.N; i++ {
157 det += 2 * math.Log(c.chol.mat.Data[i*c.chol.mat.Stride+i])
162 // Solve finds the matrix m that solves A * m = b where A is represented
163 // by the Cholesky decomposition, placing the result in m.
164 func (c *Cholesky) Solve(m *Dense, b Matrix) error {
178 blas64.Trsm(blas.Left, blas.Trans, 1, c.chol.mat, m.mat)
179 blas64.Trsm(blas.Left, blas.NoTrans, 1, c.chol.mat, m.mat)
180 if c.cond > ConditionTolerance {
181 return Condition(c.cond)
186 // SolveChol finds the matrix m that solves A * m = B where A and B are represented
187 // by their Cholesky decompositions a and b, placing the result in the receiver.
188 func (a *Cholesky) SolveChol(m *Dense, b *Cholesky) error {
189 if !a.valid() || !b.valid() {
193 if a.chol.mat.N != bn {
197 m.reuseAsZeroed(bn, bn)
199 blas64.Trsm(blas.Left, blas.Trans, 1, a.chol.mat, m.mat)
200 blas64.Trsm(blas.Left, blas.NoTrans, 1, a.chol.mat, m.mat)
201 blas64.Trmm(blas.Right, blas.NoTrans, 1, b.chol.mat, m.mat)
202 if a.cond > ConditionTolerance {
203 return Condition(a.cond)
208 // SolveVec finds the vector v that solves A * v = b where A is represented
209 // by the Cholesky decomposition, placing the result in v.
210 func (c *Cholesky) SolveVec(v *VecDense, b Vector) error {
215 if br, bc := b.Dims(); br != n || bc != 1 {
218 switch rv := b.(type) {
221 return c.Solve(v.asDense(), b)
223 bmat := rv.RawVector()
231 blas64.Trsv(blas.Trans, c.chol.mat, v.mat)
232 blas64.Trsv(blas.NoTrans, c.chol.mat, v.mat)
233 if c.cond > ConditionTolerance {
234 return Condition(c.cond)
240 // RawU returns the Triangular matrix used to store the Cholesky decomposition of
241 // the original matrix A. The returned matrix should not be modified. If it is
242 // modified, the decomposition is invalid and should not be used.
243 func (c *Cholesky) RawU() Triangular {
247 // UTo extracts the n×n upper triangular matrix U from a Cholesky
248 // decomposition into dst and returns the result. If dst is nil a new
249 // TriDense is allocated.
251 func (c *Cholesky) UTo(dst *TriDense) *TriDense {
257 dst = NewTriDense(n, Upper, make([]float64, n*n))
259 dst.reuseAs(n, Upper)
265 // LTo extracts the n×n lower triangular matrix L from a Cholesky
266 // decomposition into dst and returns the result. If dst is nil a new
267 // TriDense is allocated.
269 func (c *Cholesky) LTo(dst *TriDense) *TriDense {
275 dst = NewTriDense(n, Lower, make([]float64, n*n))
277 dst.reuseAs(n, Lower)
279 dst.Copy(c.chol.TTri())
283 // ToSym reconstructs the original positive definite matrix given its
284 // Cholesky decomposition into dst and returns the result. If dst is nil
285 // a new SymDense is allocated.
286 func (c *Cholesky) ToSym(dst *SymDense) *SymDense {
292 dst = NewSymDense(n, make([]float64, n*n))
296 dst.SymOuterK(1, c.chol.T())
300 // InverseTo computes the inverse of the matrix represented by its Cholesky
301 // factorization and stores the result into s. If the factorized
302 // matrix is ill-conditioned, a Condition error will be returned.
303 // Note that matrix inversion is numerically unstable, and should generally be
304 // avoided where possible, for example by using the Solve routines.
305 func (c *Cholesky) InverseTo(s *SymDense) error {
309 // TODO(btracey): Replace this code with a direct call to Dpotri when it
311 s.reuseAs(c.chol.mat.N)
315 // chol(A^-1) = S * S^T
318 err := t.InverseTri(c.chol)
323 // Scale multiplies the original matrix A by a positive constant using
324 // its Cholesky decomposition, storing the result in-place into the receiver.
325 // That is, if the original Cholesky factorization is
327 // the updated factorization is
328 // U'^T * U' = f A = A'
329 // Scale panics if the constant is non-positive, or if the receiver is non-zero
330 // and is of a different Size from the input.
331 func (c *Cholesky) Scale(f float64, orig *Cholesky) {
336 panic("cholesky: scaling by a non-positive constant")
340 c.chol = NewTriDense(n, Upper, nil)
341 } else if c.chol.mat.N != n {
344 c.chol.ScaleTri(math.Sqrt(f), orig.chol)
345 c.cond = orig.cond // Scaling by a positive constant does not change the condition number.
348 // ExtendVecSym computes the Cholesky decomposition of the original matrix A,
349 // whose Cholesky decomposition is in a, extended by a the n×1 vector v according to
352 // where k = v[n-1] and w = v[:n-1]. The result is stored into the receiver.
353 // In order for the updated matrix to be positive definite, it must be the case
354 // that k > w' A^-1 w. If this condition does not hold then ExtendVecSym will
355 // return false and the receiver will not be updated.
357 // ExtendVecSym will panic if v.Len() != a.Size()+1 or if a does not contain
358 // a valid decomposition.
359 func (chol *Cholesky) ExtendVecSym(a *Cholesky, v Vector) (ok bool) {
362 panic(badSliceLength)
368 // The algorithm is commented here, but see also
369 // https://math.stackexchange.com/questions/955874/cholesky-factor-when-adding-a-row-and-column-to-already-factorized-matrix
370 // We have A and want to compute the Cholesky of
376 // to be the updated Cholesky, and so it must be that
377 // [A w] = [U' 0] [U c]
378 // [w' k] [c' d] [0 d]
380 // 1) A = U'U (true by the original decomposition being valid),
381 // 2) U' * c = w => c = U'^-1 w
382 // 3) c'*c + d'*d = k => d = sqrt(k-c'*c)
384 // First, compute c = U'^-1 a
385 // TODO(btracey): Replace this with CopyVec when issue 167 is fixed.
386 w := NewVecDense(n, nil)
387 for i := 0; i < n; i++ {
388 w.SetVec(i, v.At(i, 0))
392 c := NewVecDense(n, nil)
393 c.SolveVec(a.chol.T(), w)
399 d := math.Sqrt(k - dot)
401 newU := NewTriDense(n+1, Upper, nil)
403 for i := 0; i < n; i++ {
404 newU.SetTri(i, n, c.At(i, 0))
412 // SymRankOne performs a rank-1 update of the original matrix A and refactorizes
413 // its Cholesky factorization, storing the result into the receiver. That is, if
414 // in the original Cholesky factorization
416 // in the updated factorization
417 // U'^T * U' = A + alpha * x * x^T = A'.
419 // Note that when alpha is negative, the updating problem may be ill-conditioned
420 // and the results may be inaccurate, or the updated matrix A' may not be
421 // positive definite and not have a Cholesky factorization. SymRankOne returns
422 // whether the updated matrix A' is positive definite.
424 // SymRankOne updates a Cholesky factorization in O(n²) time. The Cholesky
425 // factorization computation from scratch is O(n³).
426 func (c *Cholesky) SymRankOne(orig *Cholesky, alpha float64, x Vector) (ok bool) {
431 if r, c := x.Dims(); r != n || c != 1 {
436 c.chol = NewTriDense(n, Upper, nil)
437 } else if c.chol.mat.N != n {
440 c.chol.Copy(orig.chol)
447 // Algorithms for updating and downdating the Cholesky factorization are
448 // described, for example, in
449 // - J. J. Dongarra, J. R. Bunch, C. B. Moler, G. W. Stewart: LINPACK
450 // Users' Guide. SIAM (1979), pages 10.10--10.14
452 // - P. E. Gill, G. H. Golub, W. Murray, and M. A. Saunders: Methods for
453 // modifying matrix factorizations. Mathematics of Computation 28(126)
454 // (1974), Method C3 on page 521
456 // The implementation is based on LINPACK code
457 // http://www.netlib.org/linpack/dchud.f
458 // http://www.netlib.org/linpack/dchdd.f
460 // https://icl.cs.utk.edu/lapack-forum/viewtopic.php?f=2&t=2646
462 // According to http://icl.cs.utk.edu/lapack-forum/archives/lapack/msg00301.html
463 // LINPACK is released under BSD license.
466 // - M. A. Saunders: Large-scale Linear Programming Using the Cholesky
467 // Factorization. Technical Report Stanford University (1972)
468 // http://i.stanford.edu/pub/cstr/reports/cs/tr/72/252/CS-TR-72-252.pdf
469 // - Matthias Seeger: Low rank updates for the Cholesky decomposition.
470 // EPFL Technical Report 161468 (2004)
471 // http://infoscience.epfl.ch/record/161468
473 work := getFloats(n, false)
474 defer putFloats(work)
475 var xmat blas64.Vector
476 if rv, ok := x.(RawVectorer); ok {
477 xmat = rv.RawVector()
481 xmat = tmp.RawVector()
483 blas64.Copy(n, xmat, blas64.Vector{1, work})
486 // Compute rank-1 update.
488 blas64.Scal(n, math.Sqrt(alpha), blas64.Vector{1, work})
491 stride := umat.Stride
492 for i := 0; i < n; i++ {
493 // Compute parameters of the Givens matrix that zeroes
494 // the i-th element of x.
495 c, s, r, _ := blas64.Rotg(umat.Data[i*stride+i], work[i])
497 // Multiply by -1 to have positive diagonal
503 umat.Data[i*stride+i] = r
505 // Multiply the extended factorization matrix by
506 // the Givens matrix from the left. Only
507 // the i-th row and x are modified.
509 blas64.Vector{1, umat.Data[i*stride+i+1 : i*stride+n]},
510 blas64.Vector{1, work[i+1 : n]},
518 // Compute rank-1 downdate.
519 alpha = math.Sqrt(-alpha)
521 blas64.Scal(n, alpha, blas64.Vector{1, work})
523 // Solve U^T * p = x storing the result into work.
524 ok = lapack64.Trtrs(blas.Trans, c.chol.RawTriangular(), blas64.General{
531 // The original matrix is singular. Should not happen, because
532 // the factorization is valid.
535 norm := blas64.Nrm2(n, blas64.Vector{1, work})
537 // The updated matrix is not positive definite.
540 norm = math.Sqrt((1 + norm) * (1 - norm))
541 cos := getFloats(n, false)
543 sin := getFloats(n, false)
545 for i := n - 1; i >= 0; i-- {
546 // Compute parameters of Givens matrices that zero elements of p
548 cos[i], sin[i], norm, _ = blas64.Rotg(norm, work[i])
556 stride := umat.Stride
557 for i := n - 1; i >= 0; i-- {
558 // Apply Givens matrices to U.
559 // TODO(vladimir-ch): Use workspace to avoid modifying the
560 // receiver in case an invalid factorization is created.
561 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])
562 if umat.Data[i*stride+i] == 0 {
563 // The matrix is singular (may rarely happen due to
564 // floating-point effects?).
566 } else if umat.Data[i*stride+i] < 0 {
567 // Diagonal elements should be positive. If it happens
568 // that on the i-th row the diagonal is negative,
569 // multiply U from the left by an identity matrix that
570 // has -1 on the i-th row.
571 blas64.Scal(n-i, -1, blas64.Vector{1, umat.Data[i*stride+i : i*stride+n]})
582 func (c *Cholesky) valid() bool {
583 return c.chol != nil && !c.chol.IsZero()