1 /* 2 * Copyright (C) 2023 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.numerical; 17 18 import com.irurueta.algebra.AlgebraException; 19 import com.irurueta.algebra.Matrix; 20 import com.irurueta.algebra.Utils; 21 22 /** 23 * Estimates exponential of a square matrix. 24 * This is based on Gene H. Golub and Charles F. Van Loan. Matrix Computations. 3rd ed. 1996. p. 572 25 */ 26 public class ExponentialMatrixEstimator { 27 28 /** 29 * Default error tolerance of estimated result element-wise. 30 * When this tolerance is used, algorithm achieves an error close to machine precision, but 31 * might be slightly larger than this value (which is about 1.1e-16). 32 */ 33 public static final double TOLERANCE = Math.pow(2.0, -53.0); 34 35 /** 36 * Estimates factorial values. 37 */ 38 private final DoubleFactorialEstimator factorialEstimator = new DoubleFactorialEstimator(); 39 40 /** 41 * Number of rows of matrix to be estimated. Every time the number of rows change, reused 42 * matrices are re-instantiated for computational efficiency. 43 */ 44 private int rows = 0; 45 46 /** 47 * Scaled version of provided input matrix. This instance is reused as long as provided input 48 * matrices keep the same size. 49 */ 50 private Matrix as; 51 52 /** 53 * Denominator of Padé approximant. This is reused for efficiency while provided input matrices 54 * keep the same size. 55 */ 56 private Matrix d; 57 58 /** 59 * Numerator of Padé approximant. This is reused for efficiency while provided input matrices 60 * keep the same size. 61 */ 62 private Matrix n; 63 64 /** 65 * Internal matrix reused for efficiency while provided input matrices keep the same size. 66 */ 67 private Matrix x; 68 69 /** 70 * Internal matrix reused for efficiency while provided input matrices keep the same size. 71 */ 72 private Matrix tmp; 73 74 /** 75 * Internal matrix reused for efficiency while provided input matrices keep the same size. 76 */ 77 private Matrix cX; 78 79 /** 80 * Contains copy of estimated result. This is reused for efficiency while provided input 81 * matrices keep the same size. 82 */ 83 private Matrix f; 84 85 /** 86 * Estimates exponential of provided matrix with default error tolerance. 87 * When this tolerance is used, algorithm achieves an error close to machine precision, but 88 * might be slightly larger than this value (which is about 1.1e-6). 89 * 90 * @param a matrix to be used for exponential estimation. 91 * @return estimated exponential matrix. 92 * @throws AlgebraException if there are numerical errors. 93 */ 94 public Matrix exponential(final Matrix a) throws AlgebraException { 95 return exponential(a, TOLERANCE); 96 } 97 98 /** 99 * Estimates exponential of provided matrix. 100 * Larger tolerance than default one can be used to reduce computational complexity if less 101 * accuracy is required. 102 * 103 * @param a matrix to be used for exponential estimation. 104 * @param tolerance maximum allowed absolute error tolerance element-wise. 105 * @return estimated exponential matrix. 106 * @throws AlgebraException if there are numerical errors. 107 */ 108 public Matrix exponential(final Matrix a, final double tolerance) throws AlgebraException { 109 final var result = new Matrix(a.getRows(), a.getColumns()); 110 exponential(a, result, tolerance); 111 return result; 112 } 113 114 /** 115 * Estimates exponential of provided matrix with default error tolerance. 116 * When this tolerance is used, algorithm achieves an error close to machine precision, but 117 * might be slightly larger than this value (which is about 1.1e-6). 118 * 119 * @param a matrix to be used for exponential estimation. 120 * @param result instance where result will be stored. 121 * @throws AlgebraException if there are numerical errors. 122 */ 123 public void exponential(final Matrix a, final Matrix result) throws AlgebraException { 124 exponential(a, result, TOLERANCE); 125 } 126 127 /** 128 * Estimates exponential of provided matrix. 129 * Larger tolerance than default one can be used to reduce computational complexity if less 130 * accuracy is required. 131 * 132 * @param a matrix to be used for exponential estimation. 133 * @param result instance where result will be stored. 134 * @param tolerance maximum allowed absolute error tolerance element-wise. 135 * @throws AlgebraException if there are numerical errors. 136 */ 137 public void exponential(final Matrix a, final Matrix result, final double tolerance) throws AlgebraException { 138 final var aRows = a.getRows(); 139 140 if (aRows != a.getColumns()) { 141 throw new IllegalArgumentException("Matrix must be squared"); 142 } 143 if (result.getRows() != aRows || result.getColumns() != aRows) { 144 throw new IllegalArgumentException( 145 "Result and provided matrix must have the same size"); 146 } 147 if (tolerance < 0.0) { 148 throw new IllegalArgumentException("Tolerance must be zero or greater"); 149 } 150 151 final var normMax = normmax(a); 152 final var j = Math.max(0, 1 + (int) Math.floor(Math.log(normMax) / Math.log(2.0))); 153 154 initialize(aRows); 155 156 // scaled version of A 157 as.copyFrom(a); 158 as.multiplyByScalar(1.0 / Math.pow(2.0, j)); 159 160 // find order for required tolerance 161 final var q = findQForTolerance(tolerance, normMax); 162 163 var c = 1.0; 164 for (var k = 1; k <= q; k++) { 165 c = c * (q - k + 1) / ((2 * q - k + 1) * k); 166 167 // X = As * X 168 tmp.copyFrom(x); 169 as.multiply(tmp, x); 170 171 // c * X 172 cX.copyFrom(x); 173 cX.multiplyByScalar(c); 174 175 // N = N + c * X 176 n.add(cX); 177 178 // D = D + (-1)^k * cX 179 if (k % 2 != 0) { 180 // k is odd --> (-1)^k = -1 181 cX.multiplyByScalar(-1.0); 182 } 183 // when k is even --> (-1)^k = 1 and there is no sign multiplication needed 184 d.add(cX); 185 } 186 187 // Solve DF = N for F using Gaussian elimination. 188 Utils.solve(d, n, f); 189 190 // now square j times 191 for (var k = 0; k < j; k++) { 192 // F = F^2 = F * F 193 tmp.copyFrom(f); 194 tmp.multiply(f); 195 f.copyFrom(tmp); 196 } 197 198 result.copyFrom(f); 199 } 200 201 /** 202 * Initializes matrices being reused as long as number of rows is preserved for multiple 203 * provided input matrices for efficiency purposes. 204 * 205 * @param rows number of rows. 206 * @throws AlgebraException if an error occurs during instantiation. 207 */ 208 private void initialize(final int rows) throws AlgebraException { 209 if (rows != this.rows) { 210 as = new Matrix(rows, rows); 211 d = Matrix.identity(rows, rows); 212 n = Matrix.identity(rows, rows); 213 x = Matrix.identity(rows, rows); 214 215 tmp = new Matrix(rows, rows); 216 cX = new Matrix(rows, rows); 217 218 f = new Matrix(rows, rows); 219 220 this.rows = rows; 221 } else { 222 as.initialize(0.0); 223 Matrix.identity(d); 224 Matrix.identity(n); 225 Matrix.identity(x); 226 227 tmp.initialize(0.0); 228 cX.initialize(0.0); 229 230 f.initialize(0.0); 231 } 232 } 233 234 /** 235 * Estimates relative error achieved by this algorithm for provided input values. 236 * 237 * @param p Padé approximant order of numerator. 238 * @param q Padé approximant order of denominator. 239 * @return estimated relative error. 240 */ 241 private double relativeError(final int p, final int q) { 242 final var pFact = factorialEstimator.factorial(p); 243 final var qFact = factorialEstimator.factorial(q); 244 final var pPlusQFact = factorialEstimator.factorial(p + q); 245 final var pPlusQPlusOneFact = factorialEstimator.factorial(p + q + 1); 246 247 return Math.pow(2.0, 3.0 - (p + q)) * pFact / pPlusQFact * qFact / pPlusQPlusOneFact; 248 } 249 250 /** 251 * Finds required order of Padé approximant for provided maximum allowed relative error to be 252 * achieved. 253 * 254 * @param maxRelativeError maximum allowed relative error. 255 * @return Padé approximant order. 256 */ 257 private int findQForRelativeError(final double maxRelativeError) { 258 for (var q = 0; ; q++) { 259 final var relativeError = relativeError(q, q); 260 if (relativeError <= maxRelativeError) { 261 return q; 262 } 263 } 264 } 265 266 /** 267 * Finds required order of Padé approximant for provided absolute error tolerance to be 268 * achieved. 269 * 270 * @param tolerance maximum allowed absolute error tolerance. 271 * @param normA infinite norm (maximum absolute value) of provided matrix. 272 * @return Padé approximant order. 273 */ 274 private int findQForTolerance(final double tolerance, final double normA) { 275 return findQForRelativeError(tolerance / normA); 276 } 277 278 /** 279 * Estimates infinite norm of provided matrix. 280 * Infinite norm is equivalent to the maximum absolute value of all matrix elements. 281 * 282 * @param a matrix to compute infinite norm for. 283 * @return estimated infinite norm. 284 */ 285 private static double normmax(final Matrix a) { 286 var max = 0.0; 287 var buffer = a.getBuffer(); 288 for (var v : buffer) { 289 var value = Math.abs(v); 290 if (value > max) { 291 max = value; 292 } 293 } 294 return max; 295 } 296 }