View Javadoc
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 }