simple-squiggle/node_modules/mathjs/lib/cjs/function/matrix/eigs/complexEigs.js

792 lines
24 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.createComplexEigs = createComplexEigs;
var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
var _object = require("../../../utils/object.js");
function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; }
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
function createComplexEigs(_ref) {
var addScalar = _ref.addScalar,
subtract = _ref.subtract,
flatten = _ref.flatten,
multiply = _ref.multiply,
multiplyScalar = _ref.multiplyScalar,
divideScalar = _ref.divideScalar,
sqrt = _ref.sqrt,
abs = _ref.abs,
bignumber = _ref.bignumber,
diag = _ref.diag,
inv = _ref.inv,
qr = _ref.qr,
usolve = _ref.usolve,
usolveAll = _ref.usolveAll,
equal = _ref.equal,
complex = _ref.complex,
larger = _ref.larger,
smaller = _ref.smaller,
matrixFromColumns = _ref.matrixFromColumns,
dot = _ref.dot;
/**
* @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 _iterateUntilTriangul = iterateUntilTriangular(arr, N, prec, type, findVectors),
values = _iterateUntilTriangul.values,
C = _iterateUntilTriangul.C; // 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.apply(void 0, (0, _toConsumableArray2.default)(vectors));
}
return {
values: values,
vectors: 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 = (0, _object.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 _qr = qr(arr),
Q = _qr.Q,
R = _qr.R;
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.apply(lambdas, (0, _toConsumableArray2.default)(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(function (a, b) {
return +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: 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 = [];
var _iterator = _createForOfIteratorHelper(values),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var λ = _step.value;
var _i4 = indexOf(uniqueValues, λ, equal);
if (_i4 === -1) {
uniqueValues.push(λ);
multiplicities.push(1);
} else {
multiplicities[_i4] += 1;
}
} // find eigenvectors by solving U λE = 0
// TODO replace with an iterative eigenvector algorithm
// (this one might fail for imprecise eigenvalues)
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
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(i) {
var λ = uniqueValues[i];
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[i]) {
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(function (v) {
return multiply(correction, v);
});
vectors.push.apply(vectors, (0, _toConsumableArray2.default)(solutions.map(function (v) {
return flatten(v);
})));
};
for (var i = 0; i < len; i++) {
_loop(i);
}
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++) {
var _arr$i;
(_arr$i = arr[i]).push.apply(_arr$i, (0, _toConsumableArray2.default)(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;
var _iterator2 = _createForOfIteratorHelper(arr),
_step2;
try {
for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
var sub = _step2.value;
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;
}
} catch (err) {
_iterator2.e(err);
} finally {
_iterator2.f();
}
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(function (_) {
return 2 * Math.random() - 1;
});
if (big) {
v = v.map(function (n) {
return bignumber(n);
});
}
if (cplx) {
v = v.map(function (n) {
return 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) {
var _iterator3 = _createForOfIteratorHelper(orthog),
_step3;
try {
for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
var w = _step3.value;
// v := v (w, v)/∥w∥² w
v = subtract(v, multiply(divideScalar(dot(w, v), dot(w, w)), w));
}
} catch (err) {
_iterator3.e(err);
} finally {
_iterator3.f();
}
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;
}