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. 4 5package mat 6 7import ( 8 "math" 9 10 "gonum.org/v1/gonum/blas" 11 "gonum.org/v1/gonum/blas/blas64" 12 "gonum.org/v1/gonum/lapack/lapack64" 13) 14 15const ( 16 badTriangle = "mat: invalid triangle" 17 badCholesky = "mat: invalid Cholesky factorization" 18) 19 20var ( 21 _ Matrix = (*Cholesky)(nil) 22 _ Symmetric = (*Cholesky)(nil) 23) 24 25// Cholesky is a symmetric positive definite matrix represented by its 26// Cholesky decomposition. 27// 28// The decomposition can be constructed using the Factorize method. The 29// factorization itself can be extracted using the UTo or LTo methods, and the 30// original symmetric matrix can be recovered with ToSym. 31// 32// Note that this matrix representation is useful for certain operations, in 33// particular finding solutions to linear equations. It is very inefficient 34// at other operations, in particular At is slow. 35// 36// Cholesky methods may only be called on a value that has been successfully 37// initialized by a call to Factorize that has returned true. Calls to methods 38// of an unsuccessful Cholesky factorization will panic. 39type Cholesky struct { 40 // The chol pointer must never be retained as a pointer outside the Cholesky 41 // struct, either by returning chol outside the struct or by setting it to 42 // a pointer coming from outside. The same prohibition applies to the data 43 // slice within chol. 44 chol *TriDense 45 cond float64 46} 47 48// updateCond updates the condition number of the Cholesky decomposition. If 49// norm > 0, then that norm is used as the norm of the original matrix A, otherwise 50// the norm is estimated from the decomposition. 51func (c *Cholesky) updateCond(norm float64) { 52 n := c.chol.mat.N 53 work := getFloats(3*n, false) 54 defer putFloats(work) 55 if norm < 0 { 56 // This is an approximation. By the definition of a norm, 57 // |AB| <= |A| |B|. 58 // Since A = U^T*U, we get for the condition number κ that 59 // κ(A) := |A| |A^-1| = |U^T*U| |A^-1| <= |U^T| |U| |A^-1|, 60 // so this will overestimate the condition number somewhat. 61 // The norm of the original factorized matrix cannot be stored 62 // because of update possibilities. 63 unorm := lapack64.Lantr(CondNorm, c.chol.mat, work) 64 lnorm := lapack64.Lantr(CondNormTrans, c.chol.mat, work) 65 norm = unorm * lnorm 66 } 67 sym := c.chol.asSymBlas() 68 iwork := getInts(n, false) 69 v := lapack64.Pocon(sym, norm, work, iwork) 70 putInts(iwork) 71 c.cond = 1 / v 72} 73 74// Dims returns the dimensions of the matrix. 75func (ch *Cholesky) Dims() (r, c int) { 76 if !ch.valid() { 77 panic(badCholesky) 78 } 79 r, c = ch.chol.Dims() 80 return r, c 81} 82 83// At returns the element at row i, column j. 84func (c *Cholesky) At(i, j int) float64 { 85 if !c.valid() { 86 panic(badCholesky) 87 } 88 n := c.Symmetric() 89 if uint(i) >= uint(n) { 90 panic(ErrRowAccess) 91 } 92 if uint(j) >= uint(n) { 93 panic(ErrColAccess) 94 } 95 96 var val float64 97 for k := 0; k <= min(i, j); k++ { 98 val += c.chol.at(k, i) * c.chol.at(k, j) 99 } 100 return val 101} 102 103// T returns the the receiver, the transpose of a symmetric matrix. 104func (c *Cholesky) T() Matrix { 105 return c 106} 107 108// Symmetric implements the Symmetric interface and returns the number of rows 109// in the matrix (this is also the number of columns). 110func (c *Cholesky) Symmetric() int { 111 r, _ := c.chol.Dims() 112 return r 113} 114 115// Cond returns the condition number of the factorized matrix. 116func (c *Cholesky) Cond() float64 { 117 if !c.valid() { 118 panic(badCholesky) 119 } 120 return c.cond 121} 122 123// Factorize calculates the Cholesky decomposition of the matrix A and returns 124// whether the matrix is positive definite. If Factorize returns false, the 125// factorization must not be used. 126func (c *Cholesky) Factorize(a Symmetric) (ok bool) { 127 n := a.Symmetric() 128 if c.chol == nil { 129 c.chol = NewTriDense(n, Upper, nil) 130 } else { 131 c.chol = NewTriDense(n, Upper, use(c.chol.mat.Data, n*n)) 132 } 133 copySymIntoTriangle(c.chol, a) 134 135 sym := c.chol.asSymBlas() 136 work := getFloats(c.chol.mat.N, false) 137 norm := lapack64.Lansy(CondNorm, sym, work) 138 putFloats(work) 139 _, ok = lapack64.Potrf(sym) 140 if ok { 141 c.updateCond(norm) 142 } else { 143 c.Reset() 144 } 145 return ok 146} 147 148// Reset resets the factorization so that it can be reused as the receiver of a 149// dimensionally restricted operation. 150func (c *Cholesky) Reset() { 151 if c.chol != nil { 152 c.chol.Reset() 153 } 154 c.cond = math.Inf(1) 155} 156 157// SetFromU sets the Cholesky decomposition from the given triangular matrix. 158// SetFromU panics if t is not upper triangular. Note that t is copied into, 159// not stored inside, the receiver. 160func (c *Cholesky) SetFromU(t *TriDense) { 161 n, kind := t.Triangle() 162 if kind != Upper { 163 panic("cholesky: matrix must be upper triangular") 164 } 165 if c.chol == nil { 166 c.chol = NewTriDense(n, Upper, nil) 167 } else { 168 c.chol = NewTriDense(n, Upper, use(c.chol.mat.Data, n*n)) 169 } 170 c.chol.Copy(t) 171 c.updateCond(-1) 172} 173 174// Clone makes a copy of the input Cholesky into the receiver, overwriting the 175// previous value of the receiver. Clone does not place any restrictions on receiver 176// shape. Clone panics if the input Cholesky is not the result of a valid decomposition. 177func (c *Cholesky) Clone(chol *Cholesky) { 178 if !chol.valid() { 179 panic(badCholesky) 180 } 181 n := chol.Symmetric() 182 if c.chol == nil { 183 c.chol = NewTriDense(n, Upper, nil) 184 } else { 185 c.chol = NewTriDense(n, Upper, use(c.chol.mat.Data, n*n)) 186 } 187 c.chol.Copy(chol.chol) 188 c.cond = chol.cond 189} 190 191// Det returns the determinant of the matrix that has been factorized. 192func (c *Cholesky) Det() float64 { 193 if !c.valid() { 194 panic(badCholesky) 195 } 196 return math.Exp(c.LogDet()) 197} 198 199// LogDet returns the log of the determinant of the matrix that has been factorized. 200func (c *Cholesky) LogDet() float64 { 201 if !c.valid() { 202 panic(badCholesky) 203 } 204 var det float64 205 for i := 0; i < c.chol.mat.N; i++ { 206 det += 2 * math.Log(c.chol.mat.Data[i*c.chol.mat.Stride+i]) 207 } 208 return det 209} 210 211// SolveTo finds the matrix X that solves A * X = B where A is represented 212// by the Cholesky decomposition. The result is stored in-place into dst. 213func (c *Cholesky) SolveTo(dst *Dense, b Matrix) error { 214 if !c.valid() { 215 panic(badCholesky) 216 } 217 n := c.chol.mat.N 218 bm, bn := b.Dims() 219 if n != bm { 220 panic(ErrShape) 221 } 222 223 dst.reuseAs(bm, bn) 224 if b != dst { 225 dst.Copy(b) 226 } 227 lapack64.Potrs(c.chol.mat, dst.mat) 228 if c.cond > ConditionTolerance { 229 return Condition(c.cond) 230 } 231 return nil 232} 233 234// SolveCholTo finds the matrix X that solves A * X = B where A and B are represented 235// by their Cholesky decompositions a and b. The result is stored in-place into 236// dst. 237func (a *Cholesky) SolveCholTo(dst *Dense, b *Cholesky) error { 238 if !a.valid() || !b.valid() { 239 panic(badCholesky) 240 } 241 bn := b.chol.mat.N 242 if a.chol.mat.N != bn { 243 panic(ErrShape) 244 } 245 246 dst.reuseAsZeroed(bn, bn) 247 dst.Copy(b.chol.T()) 248 blas64.Trsm(blas.Left, blas.Trans, 1, a.chol.mat, dst.mat) 249 blas64.Trsm(blas.Left, blas.NoTrans, 1, a.chol.mat, dst.mat) 250 blas64.Trmm(blas.Right, blas.NoTrans, 1, b.chol.mat, dst.mat) 251 if a.cond > ConditionTolerance { 252 return Condition(a.cond) 253 } 254 return nil 255} 256 257// SolveVecTo finds the vector X that solves A * x = b where A is represented 258// by the Cholesky decomposition. The result is stored in-place into 259// dst. 260func (c *Cholesky) SolveVecTo(dst *VecDense, b Vector) error { 261 if !c.valid() { 262 panic(badCholesky) 263 } 264 n := c.chol.mat.N 265 if br, bc := b.Dims(); br != n || bc != 1 { 266 panic(ErrShape) 267 } 268 switch rv := b.(type) { 269 default: 270 dst.reuseAs(n) 271 return c.SolveTo(dst.asDense(), b) 272 case RawVectorer: 273 bmat := rv.RawVector() 274 if dst != b { 275 dst.checkOverlap(bmat) 276 } 277 dst.reuseAs(n) 278 if dst != b { 279 dst.CopyVec(b) 280 } 281 lapack64.Potrs(c.chol.mat, dst.asGeneral()) 282 if c.cond > ConditionTolerance { 283 return Condition(c.cond) 284 } 285 return nil 286 } 287} 288 289// RawU returns the Triangular matrix used to store the Cholesky decomposition of 290// the original matrix A. The returned matrix should not be modified. If it is 291// modified, the decomposition is invalid and should not be used. 292func (c *Cholesky) RawU() Triangular { 293 return c.chol 294} 295 296// UTo extracts the n×n upper triangular matrix U from a Cholesky 297// decomposition into dst and returns the result. If dst is nil a new 298// TriDense is allocated. 299// A = U^T * U. 300func (c *Cholesky) UTo(dst *TriDense) *TriDense { 301 if !c.valid() { 302 panic(badCholesky) 303 } 304 n := c.chol.mat.N 305 if dst == nil { 306 dst = NewTriDense(n, Upper, make([]float64, n*n)) 307 } else { 308 dst.reuseAs(n, Upper) 309 } 310 dst.Copy(c.chol) 311 return dst 312} 313 314// LTo extracts the n×n lower triangular matrix L from a Cholesky 315// decomposition into dst and returns the result. If dst is nil a new 316// TriDense is allocated. 317// A = L * L^T. 318func (c *Cholesky) LTo(dst *TriDense) *TriDense { 319 if !c.valid() { 320 panic(badCholesky) 321 } 322 n := c.chol.mat.N 323 if dst == nil { 324 dst = NewTriDense(n, Lower, make([]float64, n*n)) 325 } else { 326 dst.reuseAs(n, Lower) 327 } 328 dst.Copy(c.chol.TTri()) 329 return dst 330} 331 332// ToSym reconstructs the original positive definite matrix given its 333// Cholesky decomposition into dst and returns the result. If dst is nil 334// a new SymDense is allocated. 335func (c *Cholesky) ToSym(dst *SymDense) *SymDense { 336 if !c.valid() { 337 panic(badCholesky) 338 } 339 n := c.chol.mat.N 340 if dst == nil { 341 dst = NewSymDense(n, nil) 342 } else { 343 dst.reuseAs(n) 344 } 345 // Create a TriDense representing the Cholesky factor U with dst's 346 // backing slice. 347 // Operations on u are reflected in s. 348 u := &TriDense{ 349 mat: blas64.Triangular{ 350 Uplo: blas.Upper, 351 Diag: blas.NonUnit, 352 N: n, 353 Data: dst.mat.Data, 354 Stride: dst.mat.Stride, 355 }, 356 cap: n, 357 } 358 u.Copy(c.chol) 359 // Compute the product U^T*U using the algorithm from LAPACK/TESTING/LIN/dpot01.f 360 a := u.mat.Data 361 lda := u.mat.Stride 362 bi := blas64.Implementation() 363 for k := n - 1; k >= 0; k-- { 364 a[k*lda+k] = bi.Ddot(k+1, a[k:], lda, a[k:], lda) 365 if k > 0 { 366 bi.Dtrmv(blas.Upper, blas.Trans, blas.NonUnit, k, a, lda, a[k:], lda) 367 } 368 } 369 return dst 370} 371 372// InverseTo computes the inverse of the matrix represented by its Cholesky 373// factorization and stores the result into s. If the factorized 374// matrix is ill-conditioned, a Condition error will be returned. 375// Note that matrix inversion is numerically unstable, and should generally be 376// avoided where possible, for example by using the Solve routines. 377func (c *Cholesky) InverseTo(s *SymDense) error { 378 if !c.valid() { 379 panic(badCholesky) 380 } 381 s.reuseAs(c.chol.mat.N) 382 // Create a TriDense representing the Cholesky factor U with the backing 383 // slice from s. 384 // Operations on u are reflected in s. 385 u := &TriDense{ 386 mat: blas64.Triangular{ 387 Uplo: blas.Upper, 388 Diag: blas.NonUnit, 389 N: s.mat.N, 390 Data: s.mat.Data, 391 Stride: s.mat.Stride, 392 }, 393 cap: s.mat.N, 394 } 395 u.Copy(c.chol) 396 397 _, ok := lapack64.Potri(u.mat) 398 if !ok { 399 return Condition(math.Inf(1)) 400 } 401 if c.cond > ConditionTolerance { 402 return Condition(c.cond) 403 } 404 return nil 405} 406 407// Scale multiplies the original matrix A by a positive constant using 408// its Cholesky decomposition, storing the result in-place into the receiver. 409// That is, if the original Cholesky factorization is 410// U^T * U = A 411// the updated factorization is 412// U'^T * U' = f A = A' 413// Scale panics if the constant is non-positive, or if the receiver is non-zero 414// and is of a different size from the input. 415func (c *Cholesky) Scale(f float64, orig *Cholesky) { 416 if !orig.valid() { 417 panic(badCholesky) 418 } 419 if f <= 0 { 420 panic("cholesky: scaling by a non-positive constant") 421 } 422 n := orig.Symmetric() 423 if c.chol == nil { 424 c.chol = NewTriDense(n, Upper, nil) 425 } else if c.chol.mat.N != n { 426 panic(ErrShape) 427 } 428 c.chol.ScaleTri(math.Sqrt(f), orig.chol) 429 c.cond = orig.cond // Scaling by a positive constant does not change the condition number. 430} 431 432// ExtendVecSym computes the Cholesky decomposition of the original matrix A, 433// whose Cholesky decomposition is in a, extended by a the n×1 vector v according to 434// [A w] 435// [w' k] 436// where k = v[n-1] and w = v[:n-1]. The result is stored into the receiver. 437// In order for the updated matrix to be positive definite, it must be the case 438// that k > w' A^-1 w. If this condition does not hold then ExtendVecSym will 439// return false and the receiver will not be updated. 440// 441// ExtendVecSym will panic if v.Len() != a.Symmetric()+1 or if a does not contain 442// a valid decomposition. 443func (c *Cholesky) ExtendVecSym(a *Cholesky, v Vector) (ok bool) { 444 n := a.Symmetric() 445 446 if v.Len() != n+1 { 447 panic(badSliceLength) 448 } 449 if !a.valid() { 450 panic(badCholesky) 451 } 452 453 // The algorithm is commented here, but see also 454 // https://math.stackexchange.com/questions/955874/cholesky-factor-when-adding-a-row-and-column-to-already-factorized-matrix 455 // We have A and want to compute the Cholesky of 456 // [A w] 457 // [w' k] 458 // We want 459 // [U c] 460 // [0 d] 461 // to be the updated Cholesky, and so it must be that 462 // [A w] = [U' 0] [U c] 463 // [w' k] [c' d] [0 d] 464 // Thus, we need 465 // 1) A = U'U (true by the original decomposition being valid), 466 // 2) U' * c = w => c = U'^-1 w 467 // 3) c'*c + d'*d = k => d = sqrt(k-c'*c) 468 469 // First, compute c = U'^-1 a 470 // TODO(btracey): Replace this with CopyVec when issue 167 is fixed. 471 w := NewVecDense(n, nil) 472 for i := 0; i < n; i++ { 473 w.SetVec(i, v.At(i, 0)) 474 } 475 k := v.At(n, 0) 476 477 var t VecDense 478 t.SolveVec(a.chol.T(), w) 479 480 dot := Dot(&t, &t) 481 if dot >= k { 482 return false 483 } 484 d := math.Sqrt(k - dot) 485 486 newU := NewTriDense(n+1, Upper, nil) 487 newU.Copy(a.chol) 488 for i := 0; i < n; i++ { 489 newU.SetTri(i, n, t.At(i, 0)) 490 } 491 newU.SetTri(n, n, d) 492 c.chol = newU 493 c.updateCond(-1) 494 return true 495} 496 497// SymRankOne performs a rank-1 update of the original matrix A and refactorizes 498// its Cholesky factorization, storing the result into the receiver. That is, if 499// in the original Cholesky factorization 500// U^T * U = A, 501// in the updated factorization 502// U'^T * U' = A + alpha * x * x^T = A'. 503// 504// Note that when alpha is negative, the updating problem may be ill-conditioned 505// and the results may be inaccurate, or the updated matrix A' may not be 506// positive definite and not have a Cholesky factorization. SymRankOne returns 507// whether the updated matrix A' is positive definite. 508// 509// SymRankOne updates a Cholesky factorization in O(n²) time. The Cholesky 510// factorization computation from scratch is O(n³). 511func (c *Cholesky) SymRankOne(orig *Cholesky, alpha float64, x Vector) (ok bool) { 512 if !orig.valid() { 513 panic(badCholesky) 514 } 515 n := orig.Symmetric() 516 if r, c := x.Dims(); r != n || c != 1 { 517 panic(ErrShape) 518 } 519 if orig != c { 520 if c.chol == nil { 521 c.chol = NewTriDense(n, Upper, nil) 522 } else if c.chol.mat.N != n { 523 panic(ErrShape) 524 } 525 c.chol.Copy(orig.chol) 526 } 527 528 if alpha == 0 { 529 return true 530 } 531 532 // Algorithms for updating and downdating the Cholesky factorization are 533 // described, for example, in 534 // - J. J. Dongarra, J. R. Bunch, C. B. Moler, G. W. Stewart: LINPACK 535 // Users' Guide. SIAM (1979), pages 10.10--10.14 536 // or 537 // - P. E. Gill, G. H. Golub, W. Murray, and M. A. Saunders: Methods for 538 // modifying matrix factorizations. Mathematics of Computation 28(126) 539 // (1974), Method C3 on page 521 540 // 541 // The implementation is based on LINPACK code 542 // http://www.netlib.org/linpack/dchud.f 543 // http://www.netlib.org/linpack/dchdd.f 544 // and 545 // https://icl.cs.utk.edu/lapack-forum/viewtopic.php?f=2&t=2646 546 // 547 // According to http://icl.cs.utk.edu/lapack-forum/archives/lapack/msg00301.html 548 // LINPACK is released under BSD license. 549 // 550 // See also: 551 // - M. A. Saunders: Large-scale Linear Programming Using the Cholesky 552 // Factorization. Technical Report Stanford University (1972) 553 // http://i.stanford.edu/pub/cstr/reports/cs/tr/72/252/CS-TR-72-252.pdf 554 // - Matthias Seeger: Low rank updates for the Cholesky decomposition. 555 // EPFL Technical Report 161468 (2004) 556 // http://infoscience.epfl.ch/record/161468 557 558 work := getFloats(n, false) 559 defer putFloats(work) 560 var xmat blas64.Vector 561 if rv, ok := x.(RawVectorer); ok { 562 xmat = rv.RawVector() 563 } else { 564 var tmp *VecDense 565 tmp.CopyVec(x) 566 xmat = tmp.RawVector() 567 } 568 blas64.Copy(xmat, blas64.Vector{N: n, Data: work, Inc: 1}) 569 570 if alpha > 0 { 571 // Compute rank-1 update. 572 if alpha != 1 { 573 blas64.Scal(math.Sqrt(alpha), blas64.Vector{N: n, Data: work, Inc: 1}) 574 } 575 umat := c.chol.mat 576 stride := umat.Stride 577 for i := 0; i < n; i++ { 578 // Compute parameters of the Givens matrix that zeroes 579 // the i-th element of x. 580 c, s, r, _ := blas64.Rotg(umat.Data[i*stride+i], work[i]) 581 if r < 0 { 582 // Multiply by -1 to have positive diagonal 583 // elemnts. 584 r *= -1 585 c *= -1 586 s *= -1 587 } 588 umat.Data[i*stride+i] = r 589 if i < n-1 { 590 // Multiply the extended factorization matrix by 591 // the Givens matrix from the left. Only 592 // the i-th row and x are modified. 593 blas64.Rot( 594 blas64.Vector{N: n - i - 1, Data: umat.Data[i*stride+i+1 : i*stride+n], Inc: 1}, 595 blas64.Vector{N: n - i - 1, Data: work[i+1 : n], Inc: 1}, 596 c, s) 597 } 598 } 599 c.updateCond(-1) 600 return true 601 } 602 603 // Compute rank-1 downdate. 604 alpha = math.Sqrt(-alpha) 605 if alpha != 1 { 606 blas64.Scal(alpha, blas64.Vector{N: n, Data: work, Inc: 1}) 607 } 608 // Solve U^T * p = x storing the result into work. 609 ok = lapack64.Trtrs(blas.Trans, c.chol.RawTriangular(), blas64.General{ 610 Rows: n, 611 Cols: 1, 612 Stride: 1, 613 Data: work, 614 }) 615 if !ok { 616 // The original matrix is singular. Should not happen, because 617 // the factorization is valid. 618 panic(badCholesky) 619 } 620 norm := blas64.Nrm2(blas64.Vector{N: n, Data: work, Inc: 1}) 621 if norm >= 1 { 622 // The updated matrix is not positive definite. 623 return false 624 } 625 norm = math.Sqrt((1 + norm) * (1 - norm)) 626 cos := getFloats(n, false) 627 defer putFloats(cos) 628 sin := getFloats(n, false) 629 defer putFloats(sin) 630 for i := n - 1; i >= 0; i-- { 631 // Compute parameters of Givens matrices that zero elements of p 632 // backwards. 633 cos[i], sin[i], norm, _ = blas64.Rotg(norm, work[i]) 634 if norm < 0 { 635 norm *= -1 636 cos[i] *= -1 637 sin[i] *= -1 638 } 639 } 640 umat := c.chol.mat 641 stride := umat.Stride 642 for i := n - 1; i >= 0; i-- { 643 work[i] = 0 644 // Apply Givens matrices to U. 645 // TODO(vladimir-ch): Use workspace to avoid modifying the 646 // receiver in case an invalid factorization is created. 647 blas64.Rot( 648 blas64.Vector{N: n - i, Data: work[i:n], Inc: 1}, 649 blas64.Vector{N: n - i, Data: umat.Data[i*stride+i : i*stride+n], Inc: 1}, 650 cos[i], sin[i]) 651 if umat.Data[i*stride+i] == 0 { 652 // The matrix is singular (may rarely happen due to 653 // floating-point effects?). 654 ok = false 655 } else if umat.Data[i*stride+i] < 0 { 656 // Diagonal elements should be positive. If it happens 657 // that on the i-th row the diagonal is negative, 658 // multiply U from the left by an identity matrix that 659 // has -1 on the i-th row. 660 blas64.Scal(-1, blas64.Vector{N: n - i, Data: umat.Data[i*stride+i : i*stride+n], Inc: 1}) 661 } 662 } 663 if ok { 664 c.updateCond(-1) 665 } else { 666 c.Reset() 667 } 668 return ok 669} 670 671func (c *Cholesky) valid() bool { 672 return c.chol != nil && !c.chol.IsZero() 673} 674