123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693 |
- import { clone } from '../../../utils/object.js';
- export function createComplexEigs(_ref) {
- var {
- addScalar,
- subtract,
- flatten,
- multiply,
- multiplyScalar,
- divideScalar,
- sqrt,
- abs,
- bignumber,
- diag,
- inv,
- qr,
- usolve,
- usolveAll,
- equal,
- complex,
- larger,
- smaller,
- matrixFromColumns,
- dot
- } = _ref;
- /**
- * @param {number[][]} arr the matrix to find eigenvalues of
- * @param {number} N size of the matrix
- * @param {number|BigNumber} prec precision, anything lower will be considered zero
- * @param {'number'|'BigNumber'|'Complex'} type
- * @param {boolean} findVectors should we find eigenvectors?
- *
- * @returns {{ values: number[], vectors: number[][] }}
- */
- function complexEigs(arr, N, prec, type, findVectors) {
- if (findVectors === undefined) {
- findVectors = true;
- }
- // TODO check if any row/col are zero except the diagonal
- // make sure corresponding rows and columns have similar magnitude
- // important because of numerical stability
- // MODIFIES arr by side effect!
- var R = balance(arr, N, prec, type, findVectors);
- // R is the row transformation matrix
- // arr = A' = R A R⁻¹, A is the original matrix
- // (if findVectors is false, R is undefined)
- // (And so to return to original matrix: A = R⁻¹ arr R)
- // TODO if magnitudes of elements vary over many orders,
- // move greatest elements to the top left corner
- // using similarity transformations, reduce the matrix
- // to Hessenberg form (upper triangular plus one subdiagonal row)
- // updates the transformation matrix R with new row operationsq
- // MODIFIES arr by side effect!
- reduceToHessenberg(arr, N, prec, type, findVectors, R);
- // still true that original A = R⁻¹ arr R)
- // find eigenvalues
- var {
- values,
- C
- } = iterateUntilTriangular(arr, N, prec, type, findVectors);
- // values is the list of eigenvalues, C is the column
- // transformation matrix that transforms arr, the hessenberg
- // matrix, to upper triangular
- // (So U = C⁻¹ arr C and the relationship between current arr
- // and original A is unchanged.)
- var vectors;
- if (findVectors) {
- vectors = findEigenvectors(arr, N, C, R, values, prec, type);
- vectors = matrixFromColumns(...vectors);
- }
- return {
- values,
- vectors
- };
- }
- /**
- * @param {number[][]} arr
- * @param {number} N
- * @param {number} prec
- * @param {'number'|'BigNumber'|'Complex'} type
- * @returns {number[][]}
- */
- function balance(arr, N, prec, type, findVectors) {
- var big = type === 'BigNumber';
- var cplx = type === 'Complex';
- var realzero = big ? bignumber(0) : 0;
- var one = big ? bignumber(1) : cplx ? complex(1) : 1;
- var realone = big ? bignumber(1) : 1;
- // base of the floating-point arithmetic
- var radix = big ? bignumber(10) : 2;
- var radixSq = multiplyScalar(radix, radix);
- // the diagonal transformation matrix R
- var Rdiag;
- if (findVectors) {
- Rdiag = Array(N).fill(one);
- }
- // this isn't the only time we loop thru the matrix...
- var last = false;
- while (!last) {
- // ...haha I'm joking! unless...
- last = true;
- for (var i = 0; i < N; i++) {
- // compute the taxicab norm of i-th column and row
- // TODO optimize for complex numbers
- var colNorm = realzero;
- var rowNorm = realzero;
- for (var j = 0; j < N; j++) {
- if (i === j) continue;
- var c = abs(arr[i][j]); // should be real
- colNorm = addScalar(colNorm, c);
- rowNorm = addScalar(rowNorm, c);
- }
- if (!equal(colNorm, 0) && !equal(rowNorm, 0)) {
- // find integer power closest to balancing the matrix
- // (we want to scale only by integer powers of radix,
- // so that we don't lose any precision due to round-off)
- var f = realone;
- var _c = colNorm;
- var rowDivRadix = divideScalar(rowNorm, radix);
- var rowMulRadix = multiplyScalar(rowNorm, radix);
- while (smaller(_c, rowDivRadix)) {
- _c = multiplyScalar(_c, radixSq);
- f = multiplyScalar(f, radix);
- }
- while (larger(_c, rowMulRadix)) {
- _c = divideScalar(_c, radixSq);
- f = divideScalar(f, radix);
- }
- // check whether balancing is needed
- // condition = (c + rowNorm) / f < 0.95 * (colNorm + rowNorm)
- var condition = smaller(divideScalar(addScalar(_c, rowNorm), f), multiplyScalar(addScalar(colNorm, rowNorm), 0.95));
- // apply balancing similarity transformation
- if (condition) {
- // we should loop once again to check whether
- // another rebalancing is needed
- last = false;
- var g = divideScalar(1, f);
- for (var _j = 0; _j < N; _j++) {
- if (i === _j) {
- continue;
- }
- arr[i][_j] = multiplyScalar(arr[i][_j], f);
- arr[_j][i] = multiplyScalar(arr[_j][i], g);
- }
- // keep track of transformations
- if (findVectors) {
- Rdiag[i] = multiplyScalar(Rdiag[i], f);
- }
- }
- }
- }
- }
- // return the diagonal row transformation matrix
- return diag(Rdiag);
- }
- /**
- * @param {number[][]} arr
- * @param {number} N
- * @param {number} prec
- * @param {'number'|'BigNumber'|'Complex'} type
- * @param {boolean} findVectors
- * @param {number[][]} R the row transformation matrix that will be modified
- */
- function reduceToHessenberg(arr, N, prec, type, findVectors, R) {
- var big = type === 'BigNumber';
- var cplx = type === 'Complex';
- var zero = big ? bignumber(0) : cplx ? complex(0) : 0;
- if (big) {
- prec = bignumber(prec);
- }
- for (var i = 0; i < N - 2; i++) {
- // Find the largest subdiag element in the i-th col
- var maxIndex = 0;
- var max = zero;
- for (var j = i + 1; j < N; j++) {
- var el = arr[j][i];
- if (smaller(abs(max), abs(el))) {
- max = el;
- maxIndex = j;
- }
- }
- // This col is pivoted, no need to do anything
- if (smaller(abs(max), prec)) {
- continue;
- }
- if (maxIndex !== i + 1) {
- // Interchange maxIndex-th and (i+1)-th row
- var tmp1 = arr[maxIndex];
- arr[maxIndex] = arr[i + 1];
- arr[i + 1] = tmp1;
- // Interchange maxIndex-th and (i+1)-th column
- for (var _j2 = 0; _j2 < N; _j2++) {
- var tmp2 = arr[_j2][maxIndex];
- arr[_j2][maxIndex] = arr[_j2][i + 1];
- arr[_j2][i + 1] = tmp2;
- }
- // keep track of transformations
- if (findVectors) {
- var tmp3 = R[maxIndex];
- R[maxIndex] = R[i + 1];
- R[i + 1] = tmp3;
- }
- }
- // Reduce following rows and columns
- for (var _j3 = i + 2; _j3 < N; _j3++) {
- var n = divideScalar(arr[_j3][i], max);
- if (n === 0) {
- continue;
- }
- // from j-th row subtract n-times (i+1)th row
- for (var k = 0; k < N; k++) {
- arr[_j3][k] = subtract(arr[_j3][k], multiplyScalar(n, arr[i + 1][k]));
- }
- // to (i+1)th column add n-times j-th column
- for (var _k = 0; _k < N; _k++) {
- arr[_k][i + 1] = addScalar(arr[_k][i + 1], multiplyScalar(n, arr[_k][_j3]));
- }
- // keep track of transformations
- if (findVectors) {
- for (var _k2 = 0; _k2 < N; _k2++) {
- R[_j3][_k2] = subtract(R[_j3][_k2], multiplyScalar(n, R[i + 1][_k2]));
- }
- }
- }
- }
- return R;
- }
- /**
- * @returns {{values: values, C: Matrix}}
- * @see Press, Wiliams: Numerical recipes in Fortran 77
- * @see https://en.wikipedia.org/wiki/QR_algorithm
- */
- function iterateUntilTriangular(A, N, prec, type, findVectors) {
- var big = type === 'BigNumber';
- var cplx = type === 'Complex';
- var one = big ? bignumber(1) : cplx ? complex(1) : 1;
- if (big) {
- prec = bignumber(prec);
- }
- // The Francis Algorithm
- // The core idea of this algorithm is that doing successive
- // A' = Q⁺AQ transformations will eventually converge to block-
- // upper-triangular with diagonal blocks either 1x1 or 2x2.
- // The Q here is the one from the QR decomposition, A = QR.
- // Since the eigenvalues of a block-upper-triangular matrix are
- // the eigenvalues of its diagonal blocks and we know how to find
- // eigenvalues of a 2x2 matrix, we know the eigenvalues of A.
- var arr = clone(A);
- // the list of converged eigenvalues
- var lambdas = [];
- // size of arr, which will get smaller as eigenvalues converge
- var n = N;
- // the diagonal of the block-diagonal matrix that turns
- // converged 2x2 matrices into upper triangular matrices
- var Sdiag = [];
- // N×N matrix describing the overall transformation done during the QR algorithm
- var Qtotal = findVectors ? diag(Array(N).fill(one)) : undefined;
- // n×n matrix describing the QR transformations done since last convergence
- var Qpartial = findVectors ? diag(Array(n).fill(one)) : undefined;
- // last eigenvalue converged before this many steps
- var lastConvergenceBefore = 0;
- while (lastConvergenceBefore <= 100) {
- lastConvergenceBefore += 1;
- // TODO if the convergence is slow, do something clever
- // Perform the factorization
- var k = 0; // TODO set close to an eigenvalue
- for (var i = 0; i < n; i++) {
- arr[i][i] = subtract(arr[i][i], k);
- }
- // TODO do an implicit QR transformation
- var {
- Q,
- R
- } = qr(arr);
- arr = multiply(R, Q);
- for (var _i = 0; _i < n; _i++) {
- arr[_i][_i] = addScalar(arr[_i][_i], k);
- }
- // keep track of transformations
- if (findVectors) {
- Qpartial = multiply(Qpartial, Q);
- }
- // The rightmost diagonal element converged to an eigenvalue
- if (n === 1 || smaller(abs(arr[n - 1][n - 2]), prec)) {
- lastConvergenceBefore = 0;
- lambdas.push(arr[n - 1][n - 1]);
- // keep track of transformations
- if (findVectors) {
- Sdiag.unshift([[1]]);
- inflateMatrix(Qpartial, N);
- Qtotal = multiply(Qtotal, Qpartial);
- if (n > 1) {
- Qpartial = diag(Array(n - 1).fill(one));
- }
- }
- // reduce the matrix size
- n -= 1;
- arr.pop();
- for (var _i2 = 0; _i2 < n; _i2++) {
- arr[_i2].pop();
- }
- // The rightmost diagonal 2x2 block converged
- } else if (n === 2 || smaller(abs(arr[n - 2][n - 3]), prec)) {
- lastConvergenceBefore = 0;
- var ll = eigenvalues2x2(arr[n - 2][n - 2], arr[n - 2][n - 1], arr[n - 1][n - 2], arr[n - 1][n - 1]);
- lambdas.push(...ll);
- // keep track of transformations
- if (findVectors) {
- 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));
- inflateMatrix(Qpartial, N);
- Qtotal = multiply(Qtotal, Qpartial);
- if (n > 2) {
- Qpartial = diag(Array(n - 2).fill(one));
- }
- }
- // reduce the matrix size
- n -= 2;
- arr.pop();
- arr.pop();
- for (var _i3 = 0; _i3 < n; _i3++) {
- arr[_i3].pop();
- arr[_i3].pop();
- }
- }
- if (n === 0) {
- break;
- }
- }
- // standard sorting
- lambdas.sort((a, b) => +subtract(abs(a), abs(b)));
- // the algorithm didn't converge
- if (lastConvergenceBefore > 100) {
- var err = Error('The eigenvalues failed to converge. Only found these eigenvalues: ' + lambdas.join(', '));
- err.values = lambdas;
- err.vectors = [];
- throw err;
- }
- // combine the overall QR transformation Qtotal with the subsequent
- // transformation S that turns the diagonal 2x2 blocks to upper triangular
- var C = findVectors ? multiply(Qtotal, blockDiag(Sdiag, N)) : undefined;
- return {
- values: lambdas,
- C
- };
- }
- /**
- * @param {Matrix} A hessenberg-form matrix
- * @param {number} N size of A
- * @param {Matrix} C column transformation matrix that turns A into upper triangular
- * @param {Matrix} R similarity that turns original matrix into A
- * @param {number[]} values array of eigenvalues of A
- * @param {'number'|'BigNumber'|'Complex'} type
- * @returns {number[][]} eigenvalues
- */
- function findEigenvectors(A, N, C, R, values, prec, type) {
- var Cinv = inv(C);
- var U = multiply(Cinv, A, C);
- var big = type === 'BigNumber';
- var cplx = type === 'Complex';
- var zero = big ? bignumber(0) : cplx ? complex(0) : 0;
- var one = big ? bignumber(1) : cplx ? complex(1) : 1;
- // turn values into a kind of "multiset"
- // this way it is easier to find eigenvectors
- var uniqueValues = [];
- var multiplicities = [];
- for (var λ of values) {
- var i = indexOf(uniqueValues, λ, equal);
- if (i === -1) {
- uniqueValues.push(λ);
- multiplicities.push(1);
- } else {
- multiplicities[i] += 1;
- }
- }
- // find eigenvectors by solving U − λE = 0
- // TODO replace with an iterative eigenvector algorithm
- // (this one might fail for imprecise eigenvalues)
- var vectors = [];
- var len = uniqueValues.length;
- var b = Array(N).fill(zero);
- var E = diag(Array(N).fill(one));
- // eigenvalues for which usolve failed (due to numerical error)
- var failedLambdas = [];
- var _loop = function _loop(_i4) {
- var λ = uniqueValues[_i4];
- var S = subtract(U, multiply(λ, E)); // the characteristic matrix
- var solutions = usolveAll(S, b);
- solutions.shift(); // ignore the null vector
- // looks like we missed something, try inverse iteration
- while (solutions.length < multiplicities[_i4]) {
- var approxVec = inverseIterate(S, N, solutions, prec, type);
- if (approxVec == null) {
- // no more vectors were found
- failedLambdas.push(λ);
- break;
- }
- solutions.push(approxVec);
- }
- // Transform back into original array coordinates
- var correction = multiply(inv(R), C);
- solutions = solutions.map(v => multiply(correction, v));
- vectors.push(...solutions.map(v => flatten(v)));
- };
- for (var _i4 = 0; _i4 < len; _i4++) {
- _loop(_i4);
- }
- if (failedLambdas.length !== 0) {
- var err = new Error('Failed to find eigenvectors for the following eigenvalues: ' + failedLambdas.join(', '));
- err.values = values;
- err.vectors = vectors;
- throw err;
- }
- return vectors;
- }
- /**
- * Compute the eigenvalues of an 2x2 matrix
- * @return {[number,number]}
- */
- function eigenvalues2x2(a, b, c, d) {
- // λ± = ½ trA ± ½ √( tr²A - 4 detA )
- var trA = addScalar(a, d);
- var detA = subtract(multiplyScalar(a, d), multiplyScalar(b, c));
- var x = multiplyScalar(trA, 0.5);
- var y = multiplyScalar(sqrt(subtract(multiplyScalar(trA, trA), multiplyScalar(4, detA))), 0.5);
- return [addScalar(x, y), subtract(x, y)];
- }
- /**
- * For an 2x2 matrix compute the transformation matrix S,
- * so that SAS⁻¹ is an upper triangular matrix
- * @return {[[number,number],[number,number]]}
- * @see https://math.berkeley.edu/~ogus/old/Math_54-05/webfoils/jordan.pdf
- * @see http://people.math.harvard.edu/~knill/teaching/math21b2004/exhibits/2dmatrices/index.html
- */
- function jordanBase2x2(a, b, c, d, l1, l2, prec, type) {
- var big = type === 'BigNumber';
- var cplx = type === 'Complex';
- var zero = big ? bignumber(0) : cplx ? complex(0) : 0;
- var one = big ? bignumber(1) : cplx ? complex(1) : 1;
- // matrix is already upper triangular
- // return an identity matrix
- if (smaller(abs(c), prec)) {
- return [[one, zero], [zero, one]];
- }
- // matrix is diagonalizable
- // return its eigenvectors as columns
- if (larger(abs(subtract(l1, l2)), prec)) {
- return [[subtract(l1, d), subtract(l2, d)], [c, c]];
- }
- // matrix is not diagonalizable
- // compute off-diagonal elements of N = A - λI
- // N₁₂ = 0 ⇒ S = ( N⃗₁, I⃗₁ )
- // N₁₂ ≠ 0 ⇒ S = ( N⃗₂, I⃗₂ )
- var na = subtract(a, l1);
- var nb = subtract(b, l1);
- var nc = subtract(c, l1);
- var nd = subtract(d, l1);
- if (smaller(abs(nb), prec)) {
- return [[na, one], [nc, zero]];
- } else {
- return [[nb, zero], [nd, one]];
- }
- }
- /**
- * Enlarge the matrix from n×n to N×N, setting the new
- * elements to 1 on diagonal and 0 elsewhere
- */
- function inflateMatrix(arr, N) {
- // add columns
- for (var i = 0; i < arr.length; i++) {
- arr[i].push(...Array(N - arr[i].length).fill(0));
- }
- // add rows
- for (var _i5 = arr.length; _i5 < N; _i5++) {
- arr.push(Array(N).fill(0));
- arr[_i5][_i5] = 1;
- }
- return arr;
- }
- /**
- * Create a block-diagonal matrix with the given square matrices on the diagonal
- * @param {Matrix[] | number[][][]} arr array of matrices to be placed on the diagonal
- * @param {number} N the size of the resulting matrix
- */
- function blockDiag(arr, N) {
- var M = [];
- for (var i = 0; i < N; i++) {
- M[i] = Array(N).fill(0);
- }
- var I = 0;
- for (var sub of arr) {
- var n = sub.length;
- for (var _i6 = 0; _i6 < n; _i6++) {
- for (var j = 0; j < n; j++) {
- M[I + _i6][I + j] = sub[_i6][j];
- }
- }
- I += n;
- }
- return M;
- }
- /**
- * Finds the index of an element in an array using a custom equality function
- * @template T
- * @param {Array<T>} arr array in which to search
- * @param {T} el the element to find
- * @param {function(T, T): boolean} fn the equality function, first argument is an element of `arr`, the second is always `el`
- * @returns {number} the index of `el`, or -1 when it's not in `arr`
- */
- function indexOf(arr, el, fn) {
- for (var i = 0; i < arr.length; i++) {
- if (fn(arr[i], el)) {
- return i;
- }
- }
- return -1;
- }
- /**
- * Provided a near-singular upper-triangular matrix A and a list of vectors,
- * finds an eigenvector of A with the smallest eigenvalue, which is orthogonal
- * to each vector in the list
- * @template T
- * @param {T[][]} A near-singular square matrix
- * @param {number} N dimension
- * @param {T[][]} orthog list of vectors
- * @param {number} prec epsilon
- * @param {'number'|'BigNumber'|'Complex'} type
- * @return {T[] | null} eigenvector
- *
- * @see Numerical Recipes for Fortran 77 – 11.7 Eigenvalues or Eigenvectors by Inverse Iteration
- */
- function inverseIterate(A, N, orthog, prec, type) {
- var largeNum = type === 'BigNumber' ? bignumber(1000) : 1000;
- var b; // the vector
- // you better choose a random vector before I count to five
- var i = 0;
- while (true) {
- b = randomOrthogonalVector(N, orthog, type);
- b = usolve(A, b);
- if (larger(norm(b), largeNum)) {
- break;
- }
- if (++i >= 5) {
- return null;
- }
- }
- // you better converge before I count to ten
- i = 0;
- while (true) {
- var c = usolve(A, b);
- if (smaller(norm(orthogonalComplement(b, [c])), prec)) {
- break;
- }
- if (++i >= 10) {
- return null;
- }
- b = normalize(c);
- }
- return b;
- }
- /**
- * Generates a random unit vector of dimension N, orthogonal to each vector in the list
- * @template T
- * @param {number} N dimension
- * @param {T[][]} orthog list of vectors
- * @param {'number'|'BigNumber'|'Complex'} type
- * @returns {T[]} random vector
- */
- function randomOrthogonalVector(N, orthog, type) {
- var big = type === 'BigNumber';
- var cplx = type === 'Complex';
- // generate random vector with the correct type
- var v = Array(N).fill(0).map(_ => 2 * Math.random() - 1);
- if (big) {
- v = v.map(n => bignumber(n));
- }
- if (cplx) {
- v = v.map(n => complex(n));
- }
- // project to orthogonal complement
- v = orthogonalComplement(v, orthog);
- // normalize
- return normalize(v, type);
- }
- /**
- * Project vector v to the orthogonal complement of an array of vectors
- */
- function orthogonalComplement(v, orthog) {
- for (var w of orthog) {
- // v := v − (w, v)/∥w∥² w
- v = subtract(v, multiply(divideScalar(dot(w, v), dot(w, w)), w));
- }
- return v;
- }
- /**
- * Calculate the norm of a vector.
- * We can't use math.norm because factory can't handle circular dependency.
- * Seriously, I'm really fed up with factory.
- */
- function norm(v) {
- return abs(sqrt(dot(v, v)));
- }
- /**
- * Normalize a vector
- * @template T
- * @param {T[]} v
- * @param {'number'|'BigNumber'|'Complex'} type
- * @returns {T[]} normalized vec
- */
- function normalize(v, type) {
- var big = type === 'BigNumber';
- var cplx = type === 'Complex';
- var one = big ? bignumber(1) : cplx ? complex(1) : 1;
- return multiply(divideScalar(one, norm(v)), v);
- }
- return complexEigs;
- }
|