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 }