1 /* 2 * Copyright (C) 2015 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.statistics; 17 18 import com.irurueta.algebra.*; 19 20 import java.util.Arrays; 21 22 /** 23 * Contains methods to work with multivariate normal (i.e. Gaussian) 24 * distributions. 25 */ 26 public class MultivariateNormalDist { 27 28 /** 29 * Mean value of Gaussian distribution. 30 */ 31 private double[] mu; 32 33 /** 34 * Covariance of Gaussian distribution. 35 */ 36 private Matrix cov; 37 38 /** 39 * Basis in which the covariance matrix is expressed. 40 * This value is obtained after decomposition. 41 */ 42 private Matrix covBasis; 43 44 /** 45 * Variances on each direction of the basis. 46 */ 47 private double[] variances; 48 49 /** 50 * Constructor. 51 * Creates a normal distribution of 1 dimension. 52 */ 53 public MultivariateNormalDist() { 54 this(1); 55 } 56 57 /** 58 * Constructor. 59 * Creates a multivariate normal distribution having the provided 60 * number of dimensions, with zero mean and unitary independent variances. 61 * 62 * @param dims number of dimensions. Must be greater than zero. 63 * @throws IllegalArgumentException if provided number of dimensions is 64 * zero or less. 65 */ 66 public MultivariateNormalDist(final int dims) { 67 if (dims <= 0) { 68 throw new IllegalArgumentException("number of dimensions must be greater than zero"); 69 } 70 71 mu = new double[dims]; 72 try { 73 cov = Matrix.identity(dims, dims); 74 } catch (final WrongSizeException e) { 75 throw new IllegalArgumentException("number of dimensions must be greater than zero", e); 76 } 77 } 78 79 /** 80 * Constructor. 81 * Creates a multivariate normal distribution having provided mean and 82 * covariance. 83 * 84 * @param mean array containing mean. Must have the same number of rows as 85 * provided covariance matrix 86 * @param covariance matrix containing covariance. Must be square, symmetric 87 * and positive definite (i.e. non-singular) and must have the same number 88 * of rows as provided mean. 89 * @throws IllegalArgumentException if provided mean array has length 90 * smaller than 1 or if length of mean array is not the same as the number 91 * of rows of covariance matrix. 92 * @throws InvalidCovarianceMatrixException if provided covariance matrix is 93 * not square, symmetric and positive definite (i.e. non singular). 94 */ 95 public MultivariateNormalDist(final double[] mean, final Matrix covariance) 96 throws InvalidCovarianceMatrixException { 97 setMeanAndCovariance(mean, covariance); 98 } 99 100 /** 101 * Constructor. 102 * Creates a multivariate normal distribution having provided mean and 103 * covariance. 104 * 105 * @param mean array containing mean. Must have the same number of rows as 106 * provided covariance matrix. 107 * @param covariance matrix containing covariance. Must be square, symmetric 108 * and positive definite (i.e. non-singular) and must have the same number 109 * of rows as provided mean. 110 * @param validateSymmetricPositiveDefinite true if covariance matrix must 111 * be validated to be positive definite, false to skip validation. 112 * @throws IllegalArgumentException if provided mean array has length 113 * smaller than 1 or if length of mean array is not the same as the number 114 * of rows of covariance matrix. 115 * @throws InvalidCovarianceMatrixException if provided matrix is not 116 * valid (nor square or symmetric positive definite if validation is 117 * enabled). 118 */ 119 public MultivariateNormalDist( 120 final double[] mean, final Matrix covariance, final boolean validateSymmetricPositiveDefinite) 121 throws InvalidCovarianceMatrixException { 122 setMeanAndCovariance(mean, covariance, validateSymmetricPositiveDefinite); 123 } 124 125 /** 126 * Gets array containing mean of this multivariate Gaussian distribution. 127 * 128 * @return mean of multivariate Gaussian distribution. 129 */ 130 public double[] getMean() { 131 return mu; 132 } 133 134 /** 135 * Sets mean of this multivariate Gaussian distribution. 136 * Length of provided mean must be equal to the number of rows of provided 137 * covariance, otherwise instance won't be ready. 138 * 139 * @param mu mean of multivariate Gaussian distribution. 140 * @throws IllegalArgumentException if provided array has a length smaller 141 * than 1. 142 */ 143 public void setMean(final double[] mu) { 144 if (mu.length == 0) { 145 throw new IllegalArgumentException("length of mean array must be greater than zero"); 146 } 147 this.mu = mu; 148 } 149 150 /** 151 * Gets matrix containing covariance of this multivariate Gaussian 152 * distribution. 153 * 154 * @return covariance of multivariate Gaussian distribution. 155 */ 156 public Matrix getCovariance() { 157 return new Matrix(cov); 158 } 159 160 /** 161 * Gets matrix containing covariance of this multivariate Gaussian 162 * distribution. 163 * 164 * @param result instance where covariance of multivariate Gaussian 165 * distribution will be stored. 166 */ 167 public void getCovariance(final Matrix result) { 168 cov.copyTo(result); 169 } 170 171 /** 172 * Sets covariance of this multivariate Gaussian distribution. 173 * 174 * @param cov covariance of this multivariate Gaussian distribution. 175 * @throws InvalidCovarianceMatrixException if provided matrix is not valid 176 * (not square or symmetric positive definite). 177 */ 178 public void setCovariance(final Matrix cov) throws InvalidCovarianceMatrixException { 179 setCovariance(cov, true); 180 } 181 182 /** 183 * Sets covariance of this multivariate Gaussian distribution. 184 * 185 * @param cov covariance of this multivariate Gaussian distribution. 186 * @param validateSymmetricPositiveDefinite true if matrix must be 187 * validated to be positive definite, false to skip validation. 188 * @throws InvalidCovarianceMatrixException if provided matrix is not 189 * valid (nor square or symmetric positive definite if validation is 190 * enabled). 191 */ 192 public void setCovariance(final Matrix cov, final boolean validateSymmetricPositiveDefinite) 193 throws InvalidCovarianceMatrixException { 194 if (cov.getRows() != cov.getColumns()) { 195 throw new InvalidCovarianceMatrixException("covariance matrix must be square"); 196 } 197 198 try { 199 if (validateSymmetricPositiveDefinite) { 200 final var decomposer = new CholeskyDecomposer(cov); 201 decomposer.decompose(); 202 if (!decomposer.isSPD()) { 203 throw new InvalidCovarianceMatrixException( 204 "covariance matrix must be symmetric positive definite (non singular)"); 205 } 206 } 207 208 this.cov = new Matrix(cov); 209 covBasis = null; 210 variances = null; 211 } catch (final AlgebraException e) { 212 throw new InvalidCovarianceMatrixException("covariance matrix must be square", e); 213 } 214 } 215 216 /** 217 * Sets mean and covariance of this multivariate Gaussian distribution. 218 * 219 * @param mu array containing mean. Must have the same number of rows as 220 * provided covariance matrix 221 * @param cov matrix containing covariance. Must be square, symmetric 222 * and positive definite (i.e. non-singular) and must have the same number 223 * of rows as provided mean. 224 * @throws IllegalArgumentException if provided mean array has length 225 * smaller than 1 or if length of mean array is not the same as the number 226 * of rows of covariance matrix. 227 * @throws InvalidCovarianceMatrixException if provided covariance matrix is 228 * not square, symmetric and positive definite (i.e. non singular). 229 */ 230 public final void setMeanAndCovariance(final double[] mu, final Matrix cov) 231 throws InvalidCovarianceMatrixException { 232 setMeanAndCovariance(mu, cov, true); 233 } 234 235 /** 236 * Sets mean and covariance of this multivariate Gaussian distribution. 237 * 238 * @param mu array containing mean. Must have the same number of rows as 239 * provided covariance matrix 240 * @param cov matrix containing covariance. Must be square, symmetric 241 * and positive definite (i.e. non-singular) and must have the same number 242 * of rows as provided mean. 243 * @param validateSymmetricPositiveDefinite true if matrix must be 244 * validated to be positive definite, false to skip validation. 245 * @throws IllegalArgumentException if provided mean array has length 246 * smaller than 1 or if length of mean array is not the same as the number 247 * of rows of covariance matrix. 248 * @throws InvalidCovarianceMatrixException if provided covariance matrix is 249 * not square, symmetric and positive definite (i.e. non singular). 250 */ 251 public final void setMeanAndCovariance( 252 final double[] mu, final Matrix cov, final boolean validateSymmetricPositiveDefinite) 253 throws InvalidCovarianceMatrixException { 254 if (mu.length != cov.getRows()) { 255 throw new IllegalArgumentException("mean array length must be equal to covariance number of rows"); 256 } 257 258 setCovariance(cov, validateSymmetricPositiveDefinite); 259 setMean(mu); 260 } 261 262 /** 263 * Indicates whether provided matrix is a valid covariance matrix. 264 * A valid covariance matrix must be square, symmetric and positive definite 265 * (i.e. non-singular). 266 * 267 * @param cov matrix to be checked. 268 * @return true if matrix is a valid covariance matrix, false otherwise. 269 */ 270 public static boolean isValidCovariance(final Matrix cov) { 271 if (cov.getRows() != cov.getColumns()) { 272 return false; 273 } 274 275 try { 276 final var decomposer = new CholeskyDecomposer(cov); 277 decomposer.decompose(); 278 return decomposer.isSPD(); 279 } catch (final AlgebraException e) { 280 return false; 281 } 282 } 283 284 /** 285 * Indicates whether this instance is ready for any computation, false 286 * otherwise. 287 * 288 * @return true if instance is ready, false otherwise. 289 */ 290 public boolean isReady() { 291 return mu != null && cov != null && 292 mu.length == cov.getRows(); 293 } 294 295 /** 296 * Basis containing on each column the direction of each variance in the 297 * multidimensional Gaussian distribution, which is obtained from provided 298 * covariance matrix. 299 * This value is available only after the p.d.f. has been evaluated. 300 * 301 * @return basis containing on each column the direction of each variance 302 * in the multidimensional Gaussian distribution. 303 */ 304 public Matrix getCovarianceBasis() { 305 return covBasis; 306 } 307 308 /** 309 * Array containing the amount of variance on each direction of the basis 310 * of the covariance in the multidimensional Gaussian distribution. 311 * This value is available only after the p.d.f. has been evaluated. 312 * 313 * @return variance on each direction of the basis of the covariance. 314 */ 315 public double[] getVariances() { 316 return variances; 317 } 318 319 /** 320 * Evaluates the probability density function (p.d.f.) of a multivariate 321 * Gaussian distribution having current mean and covariance at point x. 322 * 323 * @param x array containing coordinates where p.d.f. is evaluated. 324 * @return evaluation of p.d.f. 325 * @throws NotReadyException if this instance is not ready (mean and 326 * covariance have not been provided or are not valid). 327 * @throws IllegalArgumentException if provided point length is not valid. 328 * @throws DecomposerException happens if covariance is numerically 329 * unstable (i.e. contains NaNs or very large numbers). 330 * @throws RankDeficientMatrixException happens if covariance is singular. 331 */ 332 public double p(final double[] x) throws NotReadyException, DecomposerException, RankDeficientMatrixException { 333 if (!isReady()) { 334 throw new NotReadyException(); 335 } 336 337 final var k = x.length; 338 if (k != mu.length) { 339 throw new IllegalArgumentException("length of point must be equal to the length of mean"); 340 } 341 342 var detCov = 0.0; 343 try { 344 detCov = Utils.det(cov); 345 } catch (final WrongSizeException ignore) { 346 // never thrown 347 } 348 349 final var factor = 1.0 / (Math.sqrt(Math.pow(2.0 * Math.PI, k) * detCov)); 350 return factor * Math.exp(-0.5 * squaredMahalanobisDistance(x)); 351 } 352 353 /** 354 * Evaluates the cumulative distribution function (c.d.f.) of a Gaussian 355 * distribution having current mean and covariance values. 356 * The c.d.f. is equivalent to the joint probability of the multivariate 357 * Gaussian distribution of having a value less than x on each direction 358 * of the basis of independent variances obtained from covariance matrix. 359 * Because the c.d.f is a probability, it always returns values between 0.0 360 * and 1.0. 361 * NOTE: this method will resize provided basis instance if needed. 362 * 363 * @param x point where c.d.f. is evaluated. 364 * @param basis instance where is stored the basis of each direction of 365 * independent covariances, if provided. 366 * @return evaluation of c.d.f. 367 * @throws IllegalArgumentException if length of provided point is not equal 368 * to length of current mean. 369 * @throws NotReadyException if this instance is not ready (mean and 370 * covariance have not been provided or are not valid). 371 * @throws DecomposerException if covariance is numerically unstable (i.e. 372 * contains NaNs or very large numbers). 373 */ 374 public double cdf(final double[] x, final Matrix basis) throws NotReadyException, DecomposerException { 375 if (!isReady()) { 376 throw new NotReadyException(); 377 } 378 379 final var k = x.length; 380 if (k != mu.length) { 381 throw new IllegalArgumentException("length of point must be equal to the length of mean"); 382 } 383 384 var p = 1.0; 385 try { 386 processCovariance(); 387 388 if (basis != null) { 389 basis.copyFrom(covBasis); 390 } 391 392 for (int i = 0; i < k; i++) { 393 final var singleBasis = covBasis.getSubmatrixAsArray(0, i, k - 1, i); 394 final var coordX = ArrayUtils.dotProduct(x, singleBasis); 395 final var coordMu = ArrayUtils.dotProduct(mu, singleBasis); 396 p *= NormalDist.cdf(coordX, coordMu, Math.sqrt(variances[i])); 397 } 398 399 } catch (final DecomposerException e) { 400 throw e; 401 } catch (final AlgebraException ignore) { 402 // never thrown 403 } 404 405 return p; 406 } 407 408 /** 409 * Evaluates the cumulative distribution function (c.d.f.) of a Gaussian 410 * distribution having current mean and covariance values. 411 * The c.d.f. is equivalent to the joint probability of the multivariate 412 * Gaussian distribution of having a value less than x on each direction 413 * of the basis of independent variances obtained from covariance matrix. 414 * Because the c.d.f is a probability, it always returns values between 0.0 415 * and 1.0. 416 * 417 * @param x point where c.d.f. is evaluated. 418 * @return evaluation of c.d.f. 419 * @throws IllegalArgumentException if length of provided point is not equal 420 * to length of current mean. 421 * @throws NotReadyException if this instance is not ready (mean and 422 * covariance have not been provided or are not valid). 423 * @throws DecomposerException if covariance is numerically unstable (i.e. 424 * contains NaNs or very large numbers). 425 */ 426 public double cdf(double[] x) throws NotReadyException, DecomposerException { 427 return cdf(x, null); 428 } 429 430 /** 431 * Computes the joint probability of all probabilities provided in the 432 * array. The joint probability is computed by multiplying all components of 433 * the array, assuming that all probabilities are independent. 434 * 435 * @param p array containing probabilities for each independent variance 436 * direction that can be obtained from provided covariance matrix. 437 * @return joint probability. 438 */ 439 public static double jointProbability(final double[] p) { 440 var jointP = 1.0; 441 for (final var aP : p) { 442 jointP *= aP; 443 } 444 return jointP; 445 } 446 447 /** 448 * Evaluates the inverse cumulative distribution function of a multivariate 449 * Gaussian distribution for current mean and covariance values and provided 450 * probability values for each dimension of the multivariate Gaussian 451 * distribution. 452 * NOTE: this method will resize provided basis instance if needed. 453 * 454 * @param p array containing probability values to evaluate the inverse 455 * c.d.f. on each dimension. Values in the array must be between 0.0 and 456 * 1.0. 457 * @param result coordinates of the value x for which the c.d.f. has values 458 * p. 459 * @param basis instance where is stored the basis of each direction of 460 * independent covariances, if provided. 461 * @throws IllegalArgumentException if length of probabilities is not equal 462 * to mean length, or if result and length of probabilities are not equal, 463 * or if provided probabilities are not between 0.0 and 1.0. 464 * @throws NotReadyException if this instance is not ready (mean and 465 * covariance have not been provided or are not valid). 466 * @throws DecomposerException if covariance is numerically unstable (i.e. 467 * contains NaNs or very large numbers). 468 */ 469 public void invcdf(final double[] p, final double[] result, final Matrix basis) throws NotReadyException, 470 DecomposerException { 471 if (!isReady()) { 472 throw new NotReadyException("mean and covariance not provided or invalid"); 473 } 474 475 final var k = p.length; 476 if (k != mu.length) { 477 throw new IllegalArgumentException( 478 "length of probabilities must be equal to the length of mean"); 479 } 480 if (k != result.length) { 481 throw new IllegalArgumentException("length of result must be equal to the length of mean"); 482 } 483 484 try { 485 processCovariance(); 486 487 if (basis != null) { 488 basis.copyFrom(covBasis); 489 } 490 491 // initialize to mean 492 System.arraycopy(mu, 0, result, 0, k); 493 for (var i = 0; i < k; i++) { 494 final var singleBasis = covBasis.getSubmatrixAsArray(0, i, k - 1, i); 495 final double coord = NormalDist.invcdf(p[i], mu[i], Math.sqrt(variances[i])) - mu[i]; 496 // coord*singleBasis 497 ArrayUtils.multiplyByScalar(singleBasis, coord, singleBasis); 498 499 // result = mean + coord*singleBasis 500 ArrayUtils.sum(result, singleBasis, result); 501 } 502 } catch (final DecomposerException e) { 503 throw e; 504 } catch (final AlgebraException ignore) { 505 // never thrown 506 } 507 } 508 509 /** 510 * Evaluates the inverse cumulative distribution function of a multivariate 511 * Gaussian distribution for current mean and covariance values and provided 512 * probability values for each dimension of the multivariate Gaussian 513 * distribution. 514 * NOTE: this method will resize provided basis instance if needed. 515 * 516 * @param p array containing probability values to evaluate the inverse 517 * c.d.f. on each dimension. Values in the array must be between 0.0 and 518 * 1.0. 519 * @param basis instance where is stored the basis of each direction of 520 * independent covariances, if provided. 521 * @return a new array containing coordinates of the value x for which the 522 * c.d.f. has values p. 523 * @throws IllegalArgumentException if length of probabilities is not equal 524 * to mean length, or if provided probabilities are not between 0.0 and 1.0. 525 * @throws NotReadyException if this instance is not ready (mean and 526 * covariance have not been provided or are not valid). 527 * @throws DecomposerException if covariance is numerically unstable (i.e. 528 * contains NaNs or very large numbers). 529 */ 530 public double[] invcdf(final double[] p, final Matrix basis) throws NotReadyException, DecomposerException { 531 if (mu == null) { 532 throw new NotReadyException("mean not defined"); 533 } 534 535 final var result = new double[mu.length]; 536 invcdf(p, result, basis); 537 return result; 538 } 539 540 /** 541 * Evaluates the inverse cumulative distribution function of a multivariate 542 * Gaussian distribution for current mean and covariance values and provided 543 * probability values for each dimension of the multivariate Gaussian 544 * distribution. 545 * 546 * @param p array containing probability values to evaluate the inverse 547 * c.d.f. on each dimension. Values in the array must be between 0.0 and 548 * 1.0. 549 * @param result coordinates of the value x for which the c.d.f. has values 550 * p. 551 * @throws IllegalArgumentException if length of probabilities is not equal 552 * to mean length, or if result and length of probabilities are not equal, 553 * or if provided probabilities are not between 0.0 and 1.0. 554 * @throws NotReadyException if this instance is not ready (mean and 555 * covariance have not been provided or are not valid). 556 * @throws DecomposerException if covariance is numerically unstable (i.e. 557 * contains NaNs or very large numbers). 558 */ 559 public void invcdf(final double[] p, final double[] result) throws NotReadyException, DecomposerException { 560 invcdf(p, result, null); 561 } 562 563 /** 564 * Evaluates the inverse cumulative distribution function of a multivariate 565 * Gaussian distribution for current mean and covariance values and provided 566 * probability values for each dimension of the multivariate Gaussian 567 * distribution. 568 * 569 * @param p array containing probability values to evaluate the inverse 570 * c.d.f. on each dimension. Values in the array must be between 0.0 and 571 * 1.0. 572 * @return coordinates of the value x for which the c.d.f. has values 573 * * p. 574 * @throws IllegalArgumentException if length of probabilities is not equal 575 * to mean length, or if result and length of probabilities are not equal, 576 * or if provided probabilities are not between 0.0 and 1.0. 577 * @throws NotReadyException if this instance is not ready (mean and 578 * covariance have not been provided or are not valid). 579 * @throws DecomposerException if covariance is numerically unstable (i.e. 580 * contains NaNs or very large numbers). 581 */ 582 public double[] invcdf(final double[] p) throws NotReadyException, DecomposerException { 583 return invcdf(p, (Matrix) null); 584 } 585 586 /** 587 * Evaluates the inverse cumulative distribution function of a multivariate 588 * Gaussian distribution for current mean and covariance values and provided 589 * probability value. 590 * Obtained result coordinates are computed taking into account the basis 591 * of independent variances computed from current covariance matrix. 592 * NOTE: notice that the inverse cdf of a mutivariate Gaussian distribution 593 * does not have a unique solution. This method simply returns one of the 594 * possible solutions by assuming equal probabilities on each dimension. 595 * NOTE: this method will resize provided basis instance if needed. 596 * 597 * @param p probability value to evaluate the inverse c.d.f. at. This value 598 * must be between 0.0 and 1.0 599 * @param result coordinates of the value x for which the c.d.f. has value 600 * p. 601 * @param basis instance where is stored the basis of each direction of 602 * independent covariances, if provided. 603 * @throws IllegalArgumentException if provided probability value is not 604 * between 0.0 and 1.0, if length of provided result array is not equal 605 * to length of current mean. 606 * @throws NotReadyException if this instance is not ready (mean and 607 * covariance have not been provided or are not valid). 608 * @throws DecomposerException if covariance is numerically unstable (i.e. 609 * contains NaNs or very large numbers). 610 */ 611 public void invcdf(final double p, final double[] result, final Matrix basis) 612 throws NotReadyException, DecomposerException { 613 if (p <= 0.0 || p >= 1.0) { 614 throw new IllegalArgumentException("probability value must be between 0.0 and 1.0"); 615 } 616 617 if (!isReady()) { 618 throw new NotReadyException("mean and covariance not provided or invalid"); 619 } 620 621 final var k = result.length; 622 if (k != mu.length) { 623 throw new IllegalArgumentException("length of result must be equal to mean length"); 624 } 625 626 final var probs = new double[k]; 627 Arrays.fill(probs, Math.pow(p, 1.0 / k)); 628 invcdf(probs, result, basis); 629 } 630 631 /** 632 * Evaluates the inverse cumulative distribution function of a multivariate 633 * Gaussian distribution for current mean and covariance values and provided 634 * probability value. 635 * Obtained result coordinates are computed taking into account the basis 636 * of independent variances computed from current covariance matrix. 637 * NOTE: notice that the inverse cdf of a mutivariate Gaussian distribution 638 * does not have a unique solution. This method simply returns one of the 639 * possible solutions by assuming equal probabilities on each dimension. 640 * NOTE: this method will resize provided basis instance if needed. 641 * 642 * @param p probability value to evaluate the inverse c.d.f. at. This value 643 * must be between 0.0 and 1.0 644 * @param basis instance where is stored the basis of each direction of 645 * independent covariances, if provided. 646 * @return a new array containing the coordinates of the value x for which 647 * the c.d.f. has value p. 648 * @throws IllegalArgumentException if provided probability value is not 649 * between 0.0 and 1.0. 650 * @throws NotReadyException if this instance is not ready (mean and 651 * covariance have not been provided or are not valid). 652 * @throws DecomposerException f covariance is numerically unstable (i.e. 653 * contains NaNs or very large numbers). 654 */ 655 public double[] invcdf(final double p, final Matrix basis) throws NotReadyException, DecomposerException { 656 if (mu == null) { 657 throw new NotReadyException("mean not defined"); 658 } 659 660 final var result = new double[mu.length]; 661 invcdf(p, result, basis); 662 return result; 663 } 664 665 /** 666 * Evaluates the inverse cumulative distribution function of a multivariate 667 * Gaussian distribution for current mean and covariance values and provided 668 * probability value. 669 * Obtained result coordinates are computed taking into account the basis 670 * of independent variances computed from current covariance matrix. 671 * NOTE: notice that the inverse cdf of a mutivariate Gaussian distribution 672 * does not have a unique solution. This method simply returns one of the 673 * possible solutions by assuming equal probabilities on each dimension. 674 * 675 * @param p probability value to evaluate the inverse c.d.f. at. This value 676 * must be between 0.0 and 1.0 677 * @param result coordinates of the value x for which the c.d.f. has value 678 * p. 679 * @throws IllegalArgumentException if provided probability value is not 680 * between 0.0 and 1.0, if length of provided result array is not equal 681 * to length of current mean. 682 * @throws NotReadyException if this instance is not ready (mean and 683 * covariance have not been provided or are not valid). 684 * @throws DecomposerException f covariance is numerically unstable (i.e. 685 * contains NaNs or very large numbers). 686 */ 687 public void invcdf(final double p, final double[] result) throws NotReadyException, DecomposerException { 688 invcdf(p, result, null); 689 } 690 691 /** 692 * Evaluates the inverse cumulative distribution function of a multivariate 693 * Gaussian distribution for current mean and covariance values and provided 694 * probability value. 695 * Obtained result coordinates are computed taking into account the basis 696 * of independent variances computed from current covariance matrix. 697 * NOTE: notice that the inverse cdf of a mutivariate Gaussian distribution 698 * does not have a unique solution. This method simply returns one of the 699 * possible solutions by assuming equal probabilities on each dimension. 700 * 701 * @param p probability value to evaluate the inverse c.d.f. at. This value 702 * must be between 0.0 and 1.0 703 * @return a new array containing the coordinates of the value x for which 704 * the c.d.f. has value p. 705 * @throws IllegalArgumentException if provided probability value is not 706 * between 0.0 and 1.0. 707 * @throws NotReadyException if this instance is not ready (mean and 708 * covariance have not been provided or are not valid). 709 * @throws DecomposerException f covariance is numerically unstable (i.e. 710 * contains NaNs or very large numbers). 711 */ 712 public double[] invcdf(final double p) throws NotReadyException, DecomposerException { 713 return invcdf(p, (Matrix) null); 714 } 715 716 /** 717 * Computes the Mahalanobis distance of provided multivariate pot x for 718 * current mean and covariance values. 719 * 720 * @param x point where Mahalanobis distance is evaluated. 721 * @return Mahalanobis distance of provided point respect to mean. 722 * @throws DecomposerException happens if covariance is numerically 723 * unstable (i.e. contains NaNs or very large numbers). 724 * @throws RankDeficientMatrixException happens if covariance is singular. 725 */ 726 public double mahalanobisDistance(final double[] x) throws DecomposerException, RankDeficientMatrixException { 727 return Math.sqrt(squaredMahalanobisDistance(x)); 728 } 729 730 /** 731 * Computes the squared Mahalanobis distance of provided multivariate pot x 732 * for current mean and covariance values. 733 * 734 * @param x point where Mahalanobis distance is evaluated. 735 * @return Mahalanobis distance of provided point respect to mean. 736 * @throws DecomposerException happens if covariance is numerically 737 * unstable (i.e. contains NaNs or very large numbers). 738 * @throws RankDeficientMatrixException happens if covariance is singular. 739 */ 740 public double squaredMahalanobisDistance(final double[] x) throws DecomposerException, 741 RankDeficientMatrixException { 742 final var diff = ArrayUtils.subtractAndReturnNew(x, mu); 743 final var diffMatrix = Matrix.newFromArray(diff, true); 744 final var transDiffMatrix = diffMatrix.transposeAndReturnNew(); 745 746 try { 747 final var invCov = Utils.inverse(cov); 748 transDiffMatrix.multiply(invCov); 749 transDiffMatrix.multiply(diffMatrix); 750 751 } catch (final WrongSizeException ignore) { 752 // never thrown 753 } 754 755 return transDiffMatrix.getElementAtIndex(0); 756 } 757 758 /** 759 * Processes current covariance by decomposing it into a basis and its 760 * corresponding variances if needed. 761 * 762 * @throws DecomposerException happens if covariance is numerically 763 * unstable (i.e. contains NaNs or very large numbers). 764 * @throws NotReadyException never thrown because decomposer will always be 765 * ready. 766 * @throws LockedException never thrown because decomposer will never be 767 * locked. 768 * @throws NotAvailableException never thrown because first a 769 * DecomposerException will be thrown before attempting to get V or 770 * singular values. 771 */ 772 public void processCovariance() throws DecomposerException, NotReadyException, LockedException, 773 NotAvailableException { 774 if (cov == null) { 775 throw new NotReadyException("covariance must be defined"); 776 } 777 778 if (covBasis == null || variances == null) { 779 final var decomposer = new SingularValueDecomposer(cov); 780 decomposer.decompose(); 781 782 // because matrix is symmetric positive definite: 783 // And matrices U and V are orthonormal 784 // Cov = A'*A = (U*S*V')'*(U*S*V')=V*S*U'*U*S*V' = V*S^2*V', 785 786 // where matrix S is diagonal, and contains the standard deviations 787 // on each direction of the basis V, and hence S^2 is also diagonal but 788 // containing variances on each direction. 789 // The values of S^2 are the eigenvalues of Cov, and V are the 790 // eigenvectors of Cov, hence covariance can be expressed as variances 791 // on each direction of the basis V. 792 793 // matrix containing eigenvectors (basis of directions) 794 covBasis = decomposer.getV(); 795 796 // array containing the eigenvalues (variances on each direction) 797 variances = decomposer.getSingularValues(); 798 } 799 } 800 801 /** 802 * Evaluates the Jacobian and a multivariate function at a certain mean 803 * point and computes the non-linear propagation of Gaussian uncertainty 804 * through such function at such point. 805 * 806 * @param evaluator interface to evaluate a multivariate function and its 807 * Jacobian at a certain point. 808 * @param mean mean of original multivariate Gaussian distribution to be 809 * propagated. Must have the length of the number of input variables of the 810 * multivariate function to be evaluated. 811 * @param covariance covariance of original Gaussian distribution to be 812 * propagated. Must be symmetric positive definite having size NxN where N 813 * is the length of provided mean. 814 * @param result instance where propagated multiavariate Gaussian 815 * distribution will be stored. 816 * @throws WrongSizeException if evaluator returns an invalid number of 817 * variables (i.e. negative or zero). 818 * @throws InvalidCovarianceMatrixException if provided covariance matrix is 819 * not valid (i.e. is not symmetric positive definite). 820 * @see <a href="https://github.com/joansola/slamtb">propagateUncertainty.m at https://github.com/joansola/slamtb</a> 821 */ 822 public static void propagate( 823 final JacobianEvaluator evaluator, final double[] mean, final Matrix covariance, 824 final MultivariateNormalDist result) throws WrongSizeException, InvalidCovarianceMatrixException { 825 826 final var ndims = mean.length; 827 final var nvars = evaluator.getNumberOfVariables(); 828 final var evaluation = new double[nvars]; 829 final var jacobian = new Matrix(nvars, ndims); 830 evaluator.evaluate(mean, evaluation, jacobian); 831 832 // [y, Y_x] = f(x) 833 // Y = Y_x * X * Y_x' 834 final var jacobianTrans = jacobian.transposeAndReturnNew(); 835 jacobian.multiply(covariance); 836 jacobian.multiply(jacobianTrans); 837 838 // ensure that new covariance is symmetric positive definite 839 jacobian.symmetrize(); 840 841 result.setMean(evaluation); 842 result.setCovariance(jacobian, false); 843 } 844 845 /** 846 * Evaluates the Jacobian and a multivariate function at a certain mean 847 * point and computes the non-linear propagation of Gaussian uncertainty 848 * through such function at such point. 849 * 850 * @param evaluator interface to evaluate a multivariate function and its 851 * Jacobian at a certain point. 852 * @param mean mean of original multivariate Gaussian distribution to be 853 * propagated. Must have the length of the number of input variables of the 854 * multivariate function to be evaluated. 855 * @param covariance covariance of original Gaussian distribution to be 856 * propagated. Must be symmetric positive definite having size NxN where N 857 * is the length of provided mean. 858 * @return a new propagated multivariate Gaussian distribution. 859 * @throws WrongSizeException if evaluator returns an invalid number of 860 * variables (i.e. negative or zero). 861 * @throws InvalidCovarianceMatrixException if provided covariance matrix is 862 * not valid (i.e. is not symmetric positive definite). 863 * @see <a href="https://github.com/joansola/slamtb">propagateUncertainty.m at https://github.com/joansola/slamtb</a> 864 */ 865 public static MultivariateNormalDist propagate( 866 final JacobianEvaluator evaluator, final double[] mean, final Matrix covariance) 867 throws WrongSizeException, InvalidCovarianceMatrixException { 868 final var result = new MultivariateNormalDist(); 869 propagate(evaluator, mean, covariance, result); 870 return result; 871 } 872 873 /** 874 * Evaluates the Jacobian and a multivariate function at a certain mean 875 * point and computes the non-linear propagation of Gaussian uncertainty 876 * through such function at such point. 877 * 878 * @param evaluator interface to evaluate a multivariate function and its 879 * Jacobian at a certain point. 880 * @param dist multivariate Gaussian distribution to be propagated. 881 * @param result instance where propagated multivariate Gaussian 882 * distribution will be stored. 883 * @throws WrongSizeException if evaluator returns an invalid number of 884 * variables (i.e. negative or zero). 885 * @see <a href="https://github.com/joansola/slamtb">propagateUncertainty.m at https://github.com/joansola/slamtb</a> 886 */ 887 public static void propagate(final JacobianEvaluator evaluator, final MultivariateNormalDist dist, 888 final MultivariateNormalDist result) throws WrongSizeException { 889 try { 890 propagate(evaluator, dist.getMean(), dist.getCovariance(), result); 891 } catch (final InvalidCovarianceMatrixException ignore) { 892 // never thrown 893 } 894 } 895 896 /** 897 * Evaluates the Jacobian and a multivariate function at a certain mean 898 * point and computes the non-linear propagation of Gaussian uncertainty 899 * through such function at such point. 900 * 901 * @param evaluator interface to evaluate a multivariate function and its 902 * Jacobian at a certain point. 903 * @param dist multivariate Gaussian distribution to be propagated. 904 * @return a new propagated multivariate Gaussian distribution. 905 * @throws WrongSizeException if evaluator returns an invalid number of 906 * variables (i.e. negative or zero). 907 * @see <a href="https://github.com/joansola/slamtb">propagateUncertainty.m at https://github.com/joansola/slamtb</a> 908 */ 909 public static MultivariateNormalDist propagate( 910 final JacobianEvaluator evaluator, final MultivariateNormalDist dist) throws WrongSizeException { 911 final var result = new MultivariateNormalDist(); 912 propagate(evaluator, dist, result); 913 return result; 914 } 915 916 /** 917 * Evaluates the Jacobian and a multivariate function at the mean point of 918 * this distribution and computes the non-linear propagation of Gaussian 919 * uncertainty through such function at such point. 920 * 921 * @param evaluator interface to evaluate a multivariate function and its 922 * Jacobian at a certain point. 923 * @param result instance where propagated multivariate Gaussian 924 * distribution will be stored. 925 * @throws WrongSizeException if evaluator returns an invalid number of 926 * variables (i.e. negative or zero). 927 * @see <a href="https://github.com/joansola/slamtb">propagateUncertainty.m at https://github.com/joansola/slamtb</a> 928 */ 929 public void propagateThisDistribution(final JacobianEvaluator evaluator, final MultivariateNormalDist result) 930 throws WrongSizeException { 931 propagate(evaluator, this, result); 932 } 933 934 /** 935 * Evaluates the Jacobian and a multivariate function at the mean point of 936 * this distribution and computes the non-linear propagation of Gaussian 937 * uncertainty through such function at such point. 938 * 939 * @param evaluator interface to evaluate a multivariate function and its 940 * Jacobian at a certain point. 941 * @return a new propagated multivariate Gaussian distribution. 942 * @throws WrongSizeException if evaluator returns an invalid number of 943 * variables (i.e. negative or zero). 944 * @see <a href="https://github.com/joansola/slamtb">propagateUncertainty.m at https://github.com/joansola/slamtb</a> 945 */ 946 public MultivariateNormalDist propagateThisDistribution(final JacobianEvaluator evaluator) 947 throws WrongSizeException { 948 final var result = new MultivariateNormalDist(); 949 propagateThisDistribution(evaluator, result); 950 return result; 951 } 952 953 /** 954 * Interface to evaluate a multivariate function at multivariate point x to 955 * obtain multivariate result y and its corresponding jacobian at point x. 956 */ 957 public interface JacobianEvaluator { 958 /** 959 * Evaluates multivariate point 960 * 961 * @param x array containing multivariate point where function is 962 * evaluated. 963 * @param y result of evaluating multivariate point. 964 * @param jacobian jacobian of multivariate function at point x. 965 */ 966 void evaluate(final double[] x, final double[] y, final Matrix jacobian); 967 968 /** 969 * Number of variables in output of evaluated function. This is equal 970 * to the length of the array y obtained as function evaluations. 971 * 972 * @return number of variables of the function. 973 */ 974 int getNumberOfVariables(); 975 } 976 }