complexEigs.js (21144B)
1 import { clone } from '../../../utils/object.js'; 2 export function createComplexEigs(_ref) { 3 var { 4 addScalar, 5 subtract, 6 flatten, 7 multiply, 8 multiplyScalar, 9 divideScalar, 10 sqrt, 11 abs, 12 bignumber, 13 diag, 14 inv, 15 qr, 16 usolve, 17 usolveAll, 18 equal, 19 complex, 20 larger, 21 smaller, 22 matrixFromColumns, 23 dot 24 } = _ref; 25 26 /** 27 * @param {number[][]} arr the matrix to find eigenvalues of 28 * @param {number} N size of the matrix 29 * @param {number|BigNumber} prec precision, anything lower will be considered zero 30 * @param {'number'|'BigNumber'|'Complex'} type 31 * @param {boolean} findVectors should we find eigenvectors? 32 * 33 * @returns {{ values: number[], vectors: number[][] }} 34 */ 35 function complexEigs(arr, N, prec, type, findVectors) { 36 if (findVectors === undefined) { 37 findVectors = true; 38 } // TODO check if any row/col are zero except the diagonal 39 // make sure corresponding rows and columns have similar magnitude 40 // important because of numerical stability 41 // MODIFIES arr by side effect! 42 43 44 var R = balance(arr, N, prec, type, findVectors); // R is the row transformation matrix 45 // arr = A' = R A R⁻¹, A is the original matrix 46 // (if findVectors is false, R is undefined) 47 // (And so to return to original matrix: A = R⁻¹ arr R) 48 // TODO if magnitudes of elements vary over many orders, 49 // move greatest elements to the top left corner 50 // using similarity transformations, reduce the matrix 51 // to Hessenberg form (upper triangular plus one subdiagonal row) 52 // updates the transformation matrix R with new row operationsq 53 // MODIFIES arr by side effect! 54 55 reduceToHessenberg(arr, N, prec, type, findVectors, R); // still true that original A = R⁻¹ arr R) 56 // find eigenvalues 57 58 var { 59 values, 60 C 61 } = iterateUntilTriangular(arr, N, prec, type, findVectors); // values is the list of eigenvalues, C is the column 62 // transformation matrix that transforms arr, the hessenberg 63 // matrix, to upper triangular 64 // (So U = C⁻¹ arr C and the relationship between current arr 65 // and original A is unchanged.) 66 67 var vectors; 68 69 if (findVectors) { 70 vectors = findEigenvectors(arr, N, C, R, values, prec, type); 71 vectors = matrixFromColumns(...vectors); 72 } 73 74 return { 75 values, 76 vectors 77 }; 78 } 79 /** 80 * @param {number[][]} arr 81 * @param {number} N 82 * @param {number} prec 83 * @param {'number'|'BigNumber'|'Complex'} type 84 * @returns {number[][]} 85 */ 86 87 88 function balance(arr, N, prec, type, findVectors) { 89 var big = type === 'BigNumber'; 90 var cplx = type === 'Complex'; 91 var realzero = big ? bignumber(0) : 0; 92 var one = big ? bignumber(1) : cplx ? complex(1) : 1; 93 var realone = big ? bignumber(1) : 1; // base of the floating-point arithmetic 94 95 var radix = big ? bignumber(10) : 2; 96 var radixSq = multiplyScalar(radix, radix); // the diagonal transformation matrix R 97 98 var Rdiag; 99 100 if (findVectors) { 101 Rdiag = Array(N).fill(one); 102 } // this isn't the only time we loop thru the matrix... 103 104 105 var last = false; 106 107 while (!last) { 108 // ...haha I'm joking! unless... 109 last = true; 110 111 for (var i = 0; i < N; i++) { 112 // compute the taxicab norm of i-th column and row 113 // TODO optimize for complex numbers 114 var colNorm = realzero; 115 var rowNorm = realzero; 116 117 for (var j = 0; j < N; j++) { 118 if (i === j) continue; 119 var c = abs(arr[i][j]); // should be real 120 121 colNorm = addScalar(colNorm, c); 122 rowNorm = addScalar(rowNorm, c); 123 } 124 125 if (!equal(colNorm, 0) && !equal(rowNorm, 0)) { 126 // find integer power closest to balancing the matrix 127 // (we want to scale only by integer powers of radix, 128 // so that we don't lose any precision due to round-off) 129 var f = realone; 130 var _c = colNorm; 131 var rowDivRadix = divideScalar(rowNorm, radix); 132 var rowMulRadix = multiplyScalar(rowNorm, radix); 133 134 while (smaller(_c, rowDivRadix)) { 135 _c = multiplyScalar(_c, radixSq); 136 f = multiplyScalar(f, radix); 137 } 138 139 while (larger(_c, rowMulRadix)) { 140 _c = divideScalar(_c, radixSq); 141 f = divideScalar(f, radix); 142 } // check whether balancing is needed 143 // condition = (c + rowNorm) / f < 0.95 * (colNorm + rowNorm) 144 145 146 var condition = smaller(divideScalar(addScalar(_c, rowNorm), f), multiplyScalar(addScalar(colNorm, rowNorm), 0.95)); // apply balancing similarity transformation 147 148 if (condition) { 149 // we should loop once again to check whether 150 // another rebalancing is needed 151 last = false; 152 var g = divideScalar(1, f); 153 154 for (var _j = 0; _j < N; _j++) { 155 if (i === _j) { 156 continue; 157 } 158 159 arr[i][_j] = multiplyScalar(arr[i][_j], f); 160 arr[_j][i] = multiplyScalar(arr[_j][i], g); 161 } // keep track of transformations 162 163 164 if (findVectors) { 165 Rdiag[i] = multiplyScalar(Rdiag[i], f); 166 } 167 } 168 } 169 } 170 } // return the diagonal row transformation matrix 171 172 173 return diag(Rdiag); 174 } 175 /** 176 * @param {number[][]} arr 177 * @param {number} N 178 * @param {number} prec 179 * @param {'number'|'BigNumber'|'Complex'} type 180 * @param {boolean} findVectors 181 * @param {number[][]} R the row transformation matrix that will be modified 182 */ 183 184 185 function reduceToHessenberg(arr, N, prec, type, findVectors, R) { 186 var big = type === 'BigNumber'; 187 var cplx = type === 'Complex'; 188 var zero = big ? bignumber(0) : cplx ? complex(0) : 0; 189 190 if (big) { 191 prec = bignumber(prec); 192 } 193 194 for (var i = 0; i < N - 2; i++) { 195 // Find the largest subdiag element in the i-th col 196 var maxIndex = 0; 197 var max = zero; 198 199 for (var j = i + 1; j < N; j++) { 200 var el = arr[j][i]; 201 202 if (smaller(abs(max), abs(el))) { 203 max = el; 204 maxIndex = j; 205 } 206 } // This col is pivoted, no need to do anything 207 208 209 if (smaller(abs(max), prec)) { 210 continue; 211 } 212 213 if (maxIndex !== i + 1) { 214 // Interchange maxIndex-th and (i+1)-th row 215 var tmp1 = arr[maxIndex]; 216 arr[maxIndex] = arr[i + 1]; 217 arr[i + 1] = tmp1; // Interchange maxIndex-th and (i+1)-th column 218 219 for (var _j2 = 0; _j2 < N; _j2++) { 220 var tmp2 = arr[_j2][maxIndex]; 221 arr[_j2][maxIndex] = arr[_j2][i + 1]; 222 arr[_j2][i + 1] = tmp2; 223 } // keep track of transformations 224 225 226 if (findVectors) { 227 var tmp3 = R[maxIndex]; 228 R[maxIndex] = R[i + 1]; 229 R[i + 1] = tmp3; 230 } 231 } // Reduce following rows and columns 232 233 234 for (var _j3 = i + 2; _j3 < N; _j3++) { 235 var n = divideScalar(arr[_j3][i], max); 236 237 if (n === 0) { 238 continue; 239 } // from j-th row subtract n-times (i+1)th row 240 241 242 for (var k = 0; k < N; k++) { 243 arr[_j3][k] = subtract(arr[_j3][k], multiplyScalar(n, arr[i + 1][k])); 244 } // to (i+1)th column add n-times j-th column 245 246 247 for (var _k = 0; _k < N; _k++) { 248 arr[_k][i + 1] = addScalar(arr[_k][i + 1], multiplyScalar(n, arr[_k][_j3])); 249 } // keep track of transformations 250 251 252 if (findVectors) { 253 for (var _k2 = 0; _k2 < N; _k2++) { 254 R[_j3][_k2] = subtract(R[_j3][_k2], multiplyScalar(n, R[i + 1][_k2])); 255 } 256 } 257 } 258 } 259 260 return R; 261 } 262 /** 263 * @returns {{values: values, C: Matrix}} 264 * @see Press, Wiliams: Numerical recipes in Fortran 77 265 * @see https://en.wikipedia.org/wiki/QR_algorithm 266 */ 267 268 269 function iterateUntilTriangular(A, N, prec, type, findVectors) { 270 var big = type === 'BigNumber'; 271 var cplx = type === 'Complex'; 272 var one = big ? bignumber(1) : cplx ? complex(1) : 1; 273 274 if (big) { 275 prec = bignumber(prec); 276 } // The Francis Algorithm 277 // The core idea of this algorithm is that doing successive 278 // A' = Q⁺AQ transformations will eventually converge to block- 279 // upper-triangular with diagonal blocks either 1x1 or 2x2. 280 // The Q here is the one from the QR decomposition, A = QR. 281 // Since the eigenvalues of a block-upper-triangular matrix are 282 // the eigenvalues of its diagonal blocks and we know how to find 283 // eigenvalues of a 2x2 matrix, we know the eigenvalues of A. 284 285 286 var arr = clone(A); // the list of converged eigenvalues 287 288 var lambdas = []; // size of arr, which will get smaller as eigenvalues converge 289 290 var n = N; // the diagonal of the block-diagonal matrix that turns 291 // converged 2x2 matrices into upper triangular matrices 292 293 var Sdiag = []; // N×N matrix describing the overall transformation done during the QR algorithm 294 295 var Qtotal = findVectors ? diag(Array(N).fill(one)) : undefined; // n×n matrix describing the QR transformations done since last convergence 296 297 var Qpartial = findVectors ? diag(Array(n).fill(one)) : undefined; // last eigenvalue converged before this many steps 298 299 var lastConvergenceBefore = 0; 300 301 while (lastConvergenceBefore <= 100) { 302 lastConvergenceBefore += 1; // TODO if the convergence is slow, do something clever 303 // Perform the factorization 304 305 var k = 0; // TODO set close to an eigenvalue 306 307 for (var i = 0; i < n; i++) { 308 arr[i][i] = subtract(arr[i][i], k); 309 } // TODO do an implicit QR transformation 310 311 312 var { 313 Q, 314 R 315 } = qr(arr); 316 arr = multiply(R, Q); 317 318 for (var _i = 0; _i < n; _i++) { 319 arr[_i][_i] = addScalar(arr[_i][_i], k); 320 } // keep track of transformations 321 322 323 if (findVectors) { 324 Qpartial = multiply(Qpartial, Q); 325 } // The rightmost diagonal element converged to an eigenvalue 326 327 328 if (n === 1 || smaller(abs(arr[n - 1][n - 2]), prec)) { 329 lastConvergenceBefore = 0; 330 lambdas.push(arr[n - 1][n - 1]); // keep track of transformations 331 332 if (findVectors) { 333 Sdiag.unshift([[1]]); 334 inflateMatrix(Qpartial, N); 335 Qtotal = multiply(Qtotal, Qpartial); 336 337 if (n > 1) { 338 Qpartial = diag(Array(n - 1).fill(one)); 339 } 340 } // reduce the matrix size 341 342 343 n -= 1; 344 arr.pop(); 345 346 for (var _i2 = 0; _i2 < n; _i2++) { 347 arr[_i2].pop(); 348 } // The rightmost diagonal 2x2 block converged 349 350 } else if (n === 2 || smaller(abs(arr[n - 2][n - 3]), prec)) { 351 lastConvergenceBefore = 0; 352 var ll = eigenvalues2x2(arr[n - 2][n - 2], arr[n - 2][n - 1], arr[n - 1][n - 2], arr[n - 1][n - 1]); 353 lambdas.push(...ll); // keep track of transformations 354 355 if (findVectors) { 356 Sdiag.unshift(jordanBase2x2(arr[n - 2][n - 2], arr[n - 2][n - 1], arr[n - 1][n - 2], arr[n - 1][n - 1], ll[0], ll[1], prec, type)); 357 inflateMatrix(Qpartial, N); 358 Qtotal = multiply(Qtotal, Qpartial); 359 360 if (n > 2) { 361 Qpartial = diag(Array(n - 2).fill(one)); 362 } 363 } // reduce the matrix size 364 365 366 n -= 2; 367 arr.pop(); 368 arr.pop(); 369 370 for (var _i3 = 0; _i3 < n; _i3++) { 371 arr[_i3].pop(); 372 373 arr[_i3].pop(); 374 } 375 } 376 377 if (n === 0) { 378 break; 379 } 380 } // standard sorting 381 382 383 lambdas.sort((a, b) => +subtract(abs(a), abs(b))); // the algorithm didn't converge 384 385 if (lastConvergenceBefore > 100) { 386 var err = Error('The eigenvalues failed to converge. Only found these eigenvalues: ' + lambdas.join(', ')); 387 err.values = lambdas; 388 err.vectors = []; 389 throw err; 390 } // combine the overall QR transformation Qtotal with the subsequent 391 // transformation S that turns the diagonal 2x2 blocks to upper triangular 392 393 394 var C = findVectors ? multiply(Qtotal, blockDiag(Sdiag, N)) : undefined; 395 return { 396 values: lambdas, 397 C 398 }; 399 } 400 /** 401 * @param {Matrix} A hessenberg-form matrix 402 * @param {number} N size of A 403 * @param {Matrix} C column transformation matrix that turns A into upper triangular 404 * @param {Matrix} R similarity that turns original matrix into A 405 * @param {number[]} values array of eigenvalues of A 406 * @param {'number'|'BigNumber'|'Complex'} type 407 * @returns {number[][]} eigenvalues 408 */ 409 410 411 function findEigenvectors(A, N, C, R, values, prec, type) { 412 var Cinv = inv(C); 413 var U = multiply(Cinv, A, C); 414 var big = type === 'BigNumber'; 415 var cplx = type === 'Complex'; 416 var zero = big ? bignumber(0) : cplx ? complex(0) : 0; 417 var one = big ? bignumber(1) : cplx ? complex(1) : 1; // turn values into a kind of "multiset" 418 // this way it is easier to find eigenvectors 419 420 var uniqueValues = []; 421 var multiplicities = []; 422 423 for (var λ of values) { 424 var i = indexOf(uniqueValues, λ, equal); 425 426 if (i === -1) { 427 uniqueValues.push(λ); 428 multiplicities.push(1); 429 } else { 430 multiplicities[i] += 1; 431 } 432 } // find eigenvectors by solving U − λE = 0 433 // TODO replace with an iterative eigenvector algorithm 434 // (this one might fail for imprecise eigenvalues) 435 436 437 var vectors = []; 438 var len = uniqueValues.length; 439 var b = Array(N).fill(zero); 440 var E = diag(Array(N).fill(one)); // eigenvalues for which usolve failed (due to numerical error) 441 442 var failedLambdas = []; 443 444 var _loop = function _loop(_i4) { 445 var λ = uniqueValues[_i4]; 446 var S = subtract(U, multiply(λ, E)); // the characteristic matrix 447 448 var solutions = usolveAll(S, b); 449 solutions.shift(); // ignore the null vector 450 // looks like we missed something, try inverse iteration 451 452 while (solutions.length < multiplicities[_i4]) { 453 var approxVec = inverseIterate(S, N, solutions, prec, type); 454 455 if (approxVec == null) { 456 // no more vectors were found 457 failedLambdas.push(λ); 458 break; 459 } 460 461 solutions.push(approxVec); 462 } // Transform back into original array coordinates 463 464 465 var correction = multiply(inv(R), C); 466 solutions = solutions.map(v => multiply(correction, v)); 467 vectors.push(...solutions.map(v => flatten(v))); 468 }; 469 470 for (var _i4 = 0; _i4 < len; _i4++) { 471 _loop(_i4); 472 } 473 474 if (failedLambdas.length !== 0) { 475 var err = new Error('Failed to find eigenvectors for the following eigenvalues: ' + failedLambdas.join(', ')); 476 err.values = values; 477 err.vectors = vectors; 478 throw err; 479 } 480 481 return vectors; 482 } 483 /** 484 * Compute the eigenvalues of an 2x2 matrix 485 * @return {[number,number]} 486 */ 487 488 489 function eigenvalues2x2(a, b, c, d) { 490 // λ± = ½ trA ± ½ √( tr²A - 4 detA ) 491 var trA = addScalar(a, d); 492 var detA = subtract(multiplyScalar(a, d), multiplyScalar(b, c)); 493 var x = multiplyScalar(trA, 0.5); 494 var y = multiplyScalar(sqrt(subtract(multiplyScalar(trA, trA), multiplyScalar(4, detA))), 0.5); 495 return [addScalar(x, y), subtract(x, y)]; 496 } 497 /** 498 * For an 2x2 matrix compute the transformation matrix S, 499 * so that SAS⁻¹ is an upper triangular matrix 500 * @return {[[number,number],[number,number]]} 501 * @see https://math.berkeley.edu/~ogus/old/Math_54-05/webfoils/jordan.pdf 502 * @see http://people.math.harvard.edu/~knill/teaching/math21b2004/exhibits/2dmatrices/index.html 503 */ 504 505 506 function jordanBase2x2(a, b, c, d, l1, l2, prec, type) { 507 var big = type === 'BigNumber'; 508 var cplx = type === 'Complex'; 509 var zero = big ? bignumber(0) : cplx ? complex(0) : 0; 510 var one = big ? bignumber(1) : cplx ? complex(1) : 1; // matrix is already upper triangular 511 // return an identity matrix 512 513 if (smaller(abs(c), prec)) { 514 return [[one, zero], [zero, one]]; 515 } // matrix is diagonalizable 516 // return its eigenvectors as columns 517 518 519 if (larger(abs(subtract(l1, l2)), prec)) { 520 return [[subtract(l1, d), subtract(l2, d)], [c, c]]; 521 } // matrix is not diagonalizable 522 // compute off-diagonal elements of N = A - λI 523 // N₁₂ = 0 ⇒ S = ( N⃗₁, I⃗₁ ) 524 // N₁₂ ≠ 0 ⇒ S = ( N⃗₂, I⃗₂ ) 525 526 527 var na = subtract(a, l1); 528 var nb = subtract(b, l1); 529 var nc = subtract(c, l1); 530 var nd = subtract(d, l1); 531 532 if (smaller(abs(nb), prec)) { 533 return [[na, one], [nc, zero]]; 534 } else { 535 return [[nb, zero], [nd, one]]; 536 } 537 } 538 /** 539 * Enlarge the matrix from n×n to N×N, setting the new 540 * elements to 1 on diagonal and 0 elsewhere 541 */ 542 543 544 function inflateMatrix(arr, N) { 545 // add columns 546 for (var i = 0; i < arr.length; i++) { 547 arr[i].push(...Array(N - arr[i].length).fill(0)); 548 } // add rows 549 550 551 for (var _i5 = arr.length; _i5 < N; _i5++) { 552 arr.push(Array(N).fill(0)); 553 arr[_i5][_i5] = 1; 554 } 555 556 return arr; 557 } 558 /** 559 * Create a block-diagonal matrix with the given square matrices on the diagonal 560 * @param {Matrix[] | number[][][]} arr array of matrices to be placed on the diagonal 561 * @param {number} N the size of the resulting matrix 562 */ 563 564 565 function blockDiag(arr, N) { 566 var M = []; 567 568 for (var i = 0; i < N; i++) { 569 M[i] = Array(N).fill(0); 570 } 571 572 var I = 0; 573 574 for (var sub of arr) { 575 var n = sub.length; 576 577 for (var _i6 = 0; _i6 < n; _i6++) { 578 for (var j = 0; j < n; j++) { 579 M[I + _i6][I + j] = sub[_i6][j]; 580 } 581 } 582 583 I += n; 584 } 585 586 return M; 587 } 588 /** 589 * Finds the index of an element in an array using a custom equality function 590 * @template T 591 * @param {Array<T>} arr array in which to search 592 * @param {T} el the element to find 593 * @param {function(T, T): boolean} fn the equality function, first argument is an element of `arr`, the second is always `el` 594 * @returns {number} the index of `el`, or -1 when it's not in `arr` 595 */ 596 597 598 function indexOf(arr, el, fn) { 599 for (var i = 0; i < arr.length; i++) { 600 if (fn(arr[i], el)) { 601 return i; 602 } 603 } 604 605 return -1; 606 } 607 /** 608 * Provided a near-singular upper-triangular matrix A and a list of vectors, 609 * finds an eigenvector of A with the smallest eigenvalue, which is orthogonal 610 * to each vector in the list 611 * @template T 612 * @param {T[][]} A near-singular square matrix 613 * @param {number} N dimension 614 * @param {T[][]} orthog list of vectors 615 * @param {number} prec epsilon 616 * @param {'number'|'BigNumber'|'Complex'} type 617 * @return {T[] | null} eigenvector 618 * 619 * @see Numerical Recipes for Fortran 77 – 11.7 Eigenvalues or Eigenvectors by Inverse Iteration 620 */ 621 622 623 function inverseIterate(A, N, orthog, prec, type) { 624 var largeNum = type === 'BigNumber' ? bignumber(1000) : 1000; 625 var b; // the vector 626 // you better choose a random vector before I count to five 627 628 var i = 0; 629 630 while (true) { 631 b = randomOrthogonalVector(N, orthog, type); 632 b = usolve(A, b); 633 634 if (larger(norm(b), largeNum)) { 635 break; 636 } 637 638 if (++i >= 5) { 639 return null; 640 } 641 } // you better converge before I count to ten 642 643 644 i = 0; 645 646 while (true) { 647 var c = usolve(A, b); 648 649 if (smaller(norm(orthogonalComplement(b, [c])), prec)) { 650 break; 651 } 652 653 if (++i >= 10) { 654 return null; 655 } 656 657 b = normalize(c); 658 } 659 660 return b; 661 } 662 /** 663 * Generates a random unit vector of dimension N, orthogonal to each vector in the list 664 * @template T 665 * @param {number} N dimension 666 * @param {T[][]} orthog list of vectors 667 * @param {'number'|'BigNumber'|'Complex'} type 668 * @returns {T[]} random vector 669 */ 670 671 672 function randomOrthogonalVector(N, orthog, type) { 673 var big = type === 'BigNumber'; 674 var cplx = type === 'Complex'; // generate random vector with the correct type 675 676 var v = Array(N).fill(0).map(_ => 2 * Math.random() - 1); 677 678 if (big) { 679 v = v.map(n => bignumber(n)); 680 } 681 682 if (cplx) { 683 v = v.map(n => complex(n)); 684 } // project to orthogonal complement 685 686 687 v = orthogonalComplement(v, orthog); // normalize 688 689 return normalize(v, type); 690 } 691 /** 692 * Project vector v to the orthogonal complement of an array of vectors 693 */ 694 695 696 function orthogonalComplement(v, orthog) { 697 for (var w of orthog) { 698 // v := v − (w, v)/∥w∥² w 699 v = subtract(v, multiply(divideScalar(dot(w, v), dot(w, w)), w)); 700 } 701 702 return v; 703 } 704 /** 705 * Calculate the norm of a vector. 706 * We can't use math.norm because factory can't handle circular dependency. 707 * Seriously, I'm really fed up with factory. 708 */ 709 710 711 function norm(v) { 712 return abs(sqrt(dot(v, v))); 713 } 714 /** 715 * Normalize a vector 716 * @template T 717 * @param {T[]} v 718 * @param {'number'|'BigNumber'|'Complex'} type 719 * @returns {T[]} normalized vec 720 */ 721 722 723 function normalize(v, type) { 724 var big = type === 'BigNumber'; 725 var cplx = type === 'Complex'; 726 var one = big ? bignumber(1) : cplx ? complex(1) : 1; 727 return multiply(divideScalar(one, norm(v)), v); 728 } 729 730 return complexEigs; 731 }