1 /* 2 * Copyright (C) 2012 Alberto Irurueta Carro (alberto@irurueta.com) 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.irurueta.algebra; 17 18 /** 19 * This class allows decomposition of matrices using Cholesky decomposition, 20 * which consists on retrieving a lower or upper triangular matrix so that input 21 * matrix can be decomposed as: A = L * L' = R' * R, where A is provided input 22 * matrix, L is a lower triangular matrix and R is an upper triangular matrix. 23 * Note: Cholesky decomposition can only be correctly computed on positive 24 * definite matrices. 25 */ 26 public class CholeskyDecomposer extends Decomposer { 27 28 /** 29 * Internal storage of Cholesky decomposition for provided input matrix. 30 */ 31 private Matrix r; 32 33 /** 34 * Boolean indicating whether provided input matrix is symmetric and 35 * positive definite. 36 */ 37 private boolean spd; 38 39 /** 40 * Constructor of this class. 41 */ 42 public CholeskyDecomposer() { 43 super(); 44 r = null; 45 spd = false; 46 } 47 48 /** 49 * Constructor of this class. 50 * 51 * @param inputMatrix Reference to input matrix to be decomposed. 52 */ 53 public CholeskyDecomposer(final Matrix inputMatrix) { 54 super(inputMatrix); 55 r = null; 56 spd = false; 57 } 58 59 /** 60 * Returns decomposer type corresponding to Cholesky decomposition. 61 * 62 * @return Decomposer type. 63 */ 64 @Override 65 public DecomposerType getDecomposerType() { 66 return DecomposerType.CHOLESKY_DECOMPOSITION; 67 } 68 69 /** 70 * Sets reference to input matrix to be decomposed. 71 * 72 * @param inputMatrix Reference to input matrix to be decomposed. 73 * @throws LockedException Exception thrown if attempting to call this 74 * method while this instance remains locked. 75 */ 76 @Override 77 public void setInputMatrix(final Matrix inputMatrix) throws LockedException { 78 super.setInputMatrix(inputMatrix); 79 r = null; 80 spd = false; 81 } 82 83 84 /** 85 * Returns boolean indicating whether decomposition has been computed and 86 * results can be retrieved. 87 * Attempting to retrieve decomposition results when not available, will 88 * probably raise a NotAvailableException. 89 * 90 * @return Boolean indicating whether decomposition has been computed and 91 * results can be retrieved. 92 */ 93 @Override 94 public boolean isDecompositionAvailable() { 95 return r != null; 96 } 97 98 /** 99 * This method computes Cholesky matrix decomposition, which consists on 100 * factoring provided input matrix whenever it is square, symmetric and 101 * positive definite into a lower triangulator factor such that it follows 102 * next expression: A = L * L' 103 * where A is input matrix and L is lower triangular factor (L' is its 104 * transposed). 105 * Cholesky decomposition can also be computed using Right Cholesky 106 * decomposition, in which case A = R' * R, where R is an upper triangular 107 * factor equal to L'. 108 * Both factors L and R will be accessible once Cholesky decomposition has 109 * been computed. 110 * Note: During execution of this method, this instance will remain locked, 111 * and hence attempting to set some parameters might raise a LockedException 112 * Note: After execution of this method, Cholesky decomposition will be 113 * available and operations such as retrieving L matrix factor or solving 114 * systems of linear equations will be able to be done. Attempting to call 115 * any of such operations before calling this method will raise a 116 * NotAvailableException because they require computation of Cholesky 117 * decomposition first. 118 * 119 * @throws NotReadyException Exception thrown if attempting to call this 120 * method when this instance is not ready (i.e. no input matrix has been 121 * provided). 122 * @throws LockedException Exception thrown if this decomposer is already 123 * locked before calling this method. Notice that this method will actually 124 * lock this instance while it is being executed. 125 * @throws DecomposerException Exception thrown if for any reason 126 * decomposition fails while being executed, like when convergence of 127 * results cannot be obtained, etc. 128 */ 129 @Override 130 public void decompose() throws NotReadyException, LockedException, DecomposerException { 131 132 if (isLocked()) { 133 throw new LockedException(); 134 } 135 136 if (!isReady()) { 137 throw new NotReadyException(); 138 } 139 140 final var rows = inputMatrix.getRows(); 141 final var columns = inputMatrix.getColumns(); 142 143 if (rows != columns) { 144 throw new DecomposerException(); 145 } 146 147 locked = true; 148 149 final Matrix localR; 150 try { 151 localR = new Matrix(columns, columns); 152 } catch (WrongSizeException e) { 153 throw new DecomposerException(e); 154 } 155 156 var localSpd = true; 157 double d; 158 double s; 159 160 // Main loop 161 for (var j = 0; j < columns; j++) { 162 d = 0.0; 163 for (var k = 0; k < j; k++) { 164 s = inputMatrix.getElementAt(k, j); 165 for (var i = 0; i < k; i++) { 166 s = s - localR.getElementAt(i, k) * localR.getElementAt(i, j); 167 } 168 s /= localR.getElementAt(k, k); 169 localR.setElementAt(k, j, s); 170 d += s * s; 171 localSpd &= inputMatrix.getElementAt(k, j) == inputMatrix.getElementAt(j, k); 172 } 173 d = inputMatrix.getElementAt(j, j) - d; 174 localSpd &= d > 0.0; 175 // sqrt of max(d, 0.0) 176 localR.setElementAt(j, j, Math.sqrt(Math.max(d, 0.0))); 177 for (var k = j + 1; k < columns; k++) { 178 localR.setElementAt(k, j, 0.0); 179 } 180 } 181 182 this.spd = localSpd; 183 this.r = localR; 184 185 locked = false; 186 } 187 188 /** 189 * Returns Cholesky matrix factor corresponding to a Lower triangular matrix 190 * following this expression: A = L * L'. Where A is provided input matrix 191 * that has been decomposed and L is the left lower triangular matrix 192 * factor. 193 * 194 * @return Returns Cholesky Lower triangular matrix 195 * @throws NotAvailableException Exception thrown if attempting to call this 196 * method before actually computing Cholesky decomposition. To avoid this 197 * exception call decompose() method first. 198 * @see #decompose() 199 */ 200 public Matrix getL() throws NotAvailableException { 201 if (!isDecompositionAvailable()) { 202 throw new NotAvailableException(); 203 } 204 205 return r.transposeAndReturnNew(); 206 } 207 208 /** 209 * Returns Cholesky matrix factor corresponding to an upper triangular 210 * matrix following this expression: A = R' * R. Where A is provided input 211 * matrix that has been decomposed and R is the right upper triangular 212 * matrix factor. 213 * 214 * @return Returns Cholesky upper triangular matrix 215 * @throws NotAvailableException Exception thrown if attempting to call this 216 * method before actually computing Cholesky decomposition. To avoid this 217 * exception call decompose() method first. 218 * @see #decompose() 219 */ 220 public Matrix getR() throws NotAvailableException { 221 if (!isDecompositionAvailable()) { 222 throw new NotAvailableException(); 223 } 224 225 return r; 226 } 227 228 /** 229 * Returns boolean indicating whether provided input matrix is 230 * Symmetric Positive Definite or not. 231 * Notice that if returned value is false, then Cholesky decomposition 232 * should be ignored, as Cholesky decomposition can only be computed on 233 * symmetric positive definite matrices. 234 * 235 * @return Boolean indicating whether provided input matrix is symmetric 236 * positive definite or not. 237 * @throws NotAvailableException Exception thrown if attempting to call this 238 * method before computing Cholesky decomposition. To avoid this exception 239 * call decompose() method first. 240 * @see #decompose() 241 */ 242 public boolean isSPD() throws NotAvailableException { 243 if (!isDecompositionAvailable()) { 244 throw new NotAvailableException(); 245 } 246 247 return spd; 248 } 249 250 /** 251 * Solves a linear system of equations of the following form: A * X = B. 252 * Where A is the input matrix provided for Cholesky decomposition, X is the 253 * solution to the system of equations, and B is the parameters 254 * vector/matrix. 255 * Note: This method can be reused for different b vectors/matrices without 256 * having to recompute Cholesky decomposition on the same input matrix. 257 * Note: Provided b matrix must have the same number of rows as provided 258 * input matrix A, otherwise an IllegalArgumentException will be raised 259 * Note: Provided input matrix A must be square, otherwise a 260 * WrongSizeException will be raised. 261 * Note: If provided input matrix A is not symmetric positive definite, a 262 * NonSymmetricPositiveDefiniteMatrixException will be thrown. 263 * Note: In order to be able to execute this method, a Cholesky 264 * decomposition must be available, otherwise a NotAvailableException will 265 * be raised. In order to avoid this exception call decompose() method first 266 * Note: result matrix contains solution of linear system of equations. It 267 * will be resized if provided matrix does not have proper size 268 * 269 * @param b Parameters of linear system of equations 270 * @param result instance where solution X will be stored. 271 * @throws com.irurueta.algebra.NotAvailableException if decomposition has 272 * not yet been computed. 273 * @throws com.irurueta.algebra.WrongSizeException if the number of rows of 274 * b matrix is not equal to the number of rows of input matrix provided to 275 * Cholesky decomposer. 276 * @throws com.irurueta.algebra.NonSymmetricPositiveDefiniteMatrixException if input matrix provided to Cholesky decomposer is not positive definite. 277 */ 278 public void solve(final Matrix b, final Matrix result) throws NotAvailableException, WrongSizeException, 279 NonSymmetricPositiveDefiniteMatrixException { 280 281 if (!isDecompositionAvailable()) { 282 throw new NotAvailableException(); 283 } 284 285 final var rows = inputMatrix.getRows(); 286 final var columns = inputMatrix.getColumns(); 287 final var rowsB = b.getRows(); 288 final var colsB = b.getColumns(); 289 290 if (rowsB != rows) { 291 throw new WrongSizeException(); 292 } 293 294 if (!isSPD()) { 295 throw new NonSymmetricPositiveDefiniteMatrixException(); 296 } 297 298 // resize result matrix if needed 299 if (result.getRows() != rowsB || result.getColumns() != colsB) { 300 result.resize(rowsB, colsB); 301 } 302 303 // Copy b into result matrix 304 result.copyFrom(b); 305 306 final var l = getL(); 307 308 // Solve L * Y = B 309 for (var k = 0; k < columns; k++) { 310 for (var j = 0; j < colsB; j++) { 311 for (var i = 0; i < k; i++) { 312 result.setElementAt(k, j, result.getElementAt(k, j) 313 - result.getElementAt(i, j) * l.getElementAt(k, i)); 314 } 315 result.setElementAt(k, j, result.getElementAt(k, j) / l.getElementAt(k, k)); 316 } 317 } 318 319 // Solve L' * X = Y 320 int k2; 321 for (var k = columns - 1; k >= 0; k--) { 322 k2 = k; 323 for (var j = 0; j < colsB; j++) { 324 for (var i = k2 + 1; i < columns; i++) { 325 result.setElementAt(k2, j, result.getElementAt(k2, j) 326 - result.getElementAt(i, j) * l.getElementAt(i, k2)); 327 } 328 result.setElementAt(k2, j, result.getElementAt(k2, j) / l.getElementAt(k2, k2)); 329 } 330 } 331 } 332 333 /** 334 * Solves a linear system of equations of the following form: A * X = B. 335 * Where A is the input matrix provided for Cholesky decomposition, X is the 336 * solution to the system of equations, and B is the parameters 337 * vector/matrix. 338 * Note: This method can be reused for different b vectors/matrices without 339 * having to recompute Cholesky decomposition on the same input matrix. 340 * Note: Provided b matrix must have the same number of rows as provided 341 * input matrix A, otherwise an IllegalArgumentException will be raised 342 * Note: Provided input matrix A must be square, otherwise a 343 * WrongSizeException will be raised. 344 * Note: If provided input matrix A is not symmetric positive definite, a 345 * NonSymmetricPositiveDefiniteMatrixException will be thrown. 346 * Note: In order to be able to execute this method, a Cholesky 347 * decomposition must be available, otherwise a NotAvailableException will 348 * be raised. In order to avoid this exception call decompose() method first 349 * 350 * @param b Parameters of linear system of equations 351 * @return a new matrix containing solution X. 352 * @throws com.irurueta.algebra.NotAvailableException if decomposition has 353 * not yet been computed. 354 * @throws com.irurueta.algebra.WrongSizeException if the number of rows of 355 * b matrix is not equal to the number of rows of input matrix provided to 356 * Cholesky decomposer. 357 * @throws com.irurueta.algebra.NonSymmetricPositiveDefiniteMatrixException if input matrix provided to Cholesky decomposer is not positive definite. 358 */ 359 public Matrix solve(final Matrix b) throws NotAvailableException, WrongSizeException, 360 NonSymmetricPositiveDefiniteMatrixException { 361 362 final var columns = inputMatrix.getColumns(); 363 final var colsB = b.getColumns(); 364 final var out = new Matrix(columns, colsB); 365 solve(b, out); 366 return out; 367 } 368 }