diff --git a/src/function/arithmetic/norm.js b/src/function/arithmetic/norm.js index 9ef2632b1f..7a86097d58 100644 --- a/src/function/arithmetic/norm.js +++ b/src/function/arithmetic/norm.js @@ -240,7 +240,7 @@ export const createNorm = /* #__PURE__ */ factory( const tx = ctranspose(x) const squaredX = multiply(tx, x) const eigenVals = eigs(squaredX).values - const rho = eigenVals.get([eigenVals.size()[0] - 1]) + const rho = eigenVals[eigenVals.length - 1] return abs(sqrt(rho)) } diff --git a/src/function/complex/im.js b/src/function/complex/im.js index d51fa29b77..b5e5faa465 100644 --- a/src/function/complex/im.js +++ b/src/function/complex/im.js @@ -41,6 +41,10 @@ export const createIm = /* #__PURE__ */ factory(name, dependencies, ({ typed }) return x.mul(0) }, + Fraction: function (x) { + return x.mul(0) + }, + Complex: function (x) { return x.im }, diff --git a/src/function/complex/re.js b/src/function/complex/re.js index a22d1a68a1..3b98ecb108 100644 --- a/src/function/complex/re.js +++ b/src/function/complex/re.js @@ -41,6 +41,10 @@ export const createRe = /* #__PURE__ */ factory(name, dependencies, ({ typed }) return x }, + Fraction: function (x) { + return x + }, + Complex: function (x) { return x.re }, diff --git a/src/function/matrix/eigs.js b/src/function/matrix/eigs.js index 36b85d6808..f061ee813a 100644 --- a/src/function/matrix/eigs.js +++ b/src/function/matrix/eigs.js @@ -1,373 +1,196 @@ -import { clone } from '../../utils/object.js' import { factory } from '../../utils/factory.js' import { format } from '../../utils/string.js' +import { createComplex } from './eigs/complex.js' +import { createRealSymmetric } from './eigs/realSymetric.js' +import { typeOf, isNumber, isBigNumber, isComplex, isFraction } from '../../utils/is.js' const name = 'eigs' -const dependencies = ['config', 'typed', 'matrix', 'addScalar', 'equal', 'subtract', 'abs', 'atan', 'cos', 'sin', 'multiplyScalar', 'inv', 'bignumber', 'multiply', 'add'] -export const createEigs = /* #__PURE__ */ factory(name, dependencies, ({ config, typed, matrix, addScalar, subtract, equal, abs, atan, cos, sin, multiplyScalar, inv, bignumber, multiply, add }) => { +// The absolute state of math.js's dependency system: +const dependencies = ['config', 'typed', 'matrix', 'addScalar', 'equal', 'subtract', 'abs', 'atan', 'cos', 'sin', 'multiplyScalar', 'divideScalar', 'inv', 'bignumber', 'multiply', 'add', 'larger', 'column', 'flatten', 'number', 'complex', 'sqrt', 'diag', 'qr', 'usolveAll', 'im', 're', 'smaller', 'round', 'log10', 'transpose'] +export const createEigs = /* #__PURE__ */ factory(name, dependencies, ({ config, typed, matrix, addScalar, subtract, equal, abs, atan, cos, sin, multiplyScalar, divideScalar, inv, bignumber, multiply, add, larger, column, flatten, number, complex, sqrt, diag, qr, usolveAll, im, re, smaller, round, log10, transpose }) => { + const doRealSymetric = createRealSymmetric({ config, addScalar, subtract, column, flatten, equal, abs, atan, cos, sin, multiplyScalar, inv, bignumber, complex, multiply, add }) + const doComplex = createComplex({ config, addScalar, subtract, multiply, multiplyScalar, flatten, divideScalar, sqrt, abs, bignumber, diag, qr, inv, usolveAll, equal, complex, larger, smaller, round, log10, transpose }) + /** - * Compute eigenvalue and eigenvector of a real symmetric matrix. - * Only applicable to two dimensional symmetric matrices. Uses Jacobi - * Algorithm. Matrix containing mixed type ('number', 'bignumber', 'fraction') - * of elements are not supported. Input matrix or 2D array should contain all elements - * of either 'number', 'bignumber' or 'fraction' type. For 'number' and 'fraction', the - * eigenvalues are of 'number' type. For 'bignumber' the eigenvalues are of ''bignumber' type. - * Eigenvectors are always of 'number' type. + * Compute eigenvalues and eigenvectors of a matrix. The eigenvalues are sorted by their absolute value, ascending. + * An eigenvalue with multiplicity k will be listed k times. The eigenvectors are returned as columns of a matrix – + * the eigenvector that belongs to the j-th eigenvalue in the list (eg. `values[j]`) is the j-th column (eg. `column(vectors, j)`). + * If the algorithm fails to converge, it will throw an error – in that case, however, you may still find useful information + * in `err.values` and `err.vectors`. * * Syntax: * - * math.eigs(x) + * math.eigs(x, [prec]) * * Examples: * + * const { eigs, multiply, column, transpose } = math * const H = [[5, 2.3], [2.3, 1]] - * const ans = math.eigs(H) // returns {values: [E1,E2...sorted], vectors: [v1,v2.... corresponding vectors as columns]} + * const ans = eigs(H) // returns {values: [E1,E2...sorted], vectors: [v1,v2.... corresponding vectors as columns]} * const E = ans.values * const U = ans.vectors - * math.multiply(H, math.column(U, 0)) // returns math.multiply(E[0], math.column(U, 0)) - * const UTxHxU = math.multiply(math.transpose(U), H, U) // rotates H to the eigen-representation + * multiply(H, column(U, 0)) // returns multiply(E[0], column(U, 0)) + * const UTxHxU = multiply(transpose(U), H, U) // diagonalizes H * E[0] == UTxHxU[0][0] // returns true + * * See also: * * inv * * @param {Array | Matrix} x Matrix to be diagonalized - * @return {{values: Array, vectors: Array} | {values: Matrix, vectors: Matrix}} Object containing eigenvalues (Array or Matrix) and eigenvectors (2D Array/Matrix with eigenvectors as columns). + * + * @param {number | BigNumber} [prec] Precision, default value: 1e-15 + * @return {{values: Array, vectors: Matrix}} Object containing an array of eigenvalues and a matrix with eigenvectors as columns. + * */ return typed('eigs', { Array: function (x) { - // check array size const mat = matrix(x) - const size = mat.size() - if (size.length !== 2 || size[0] !== size[1]) { - throw new RangeError('Matrix must be square ' + - '(size: ' + format(size) + ')') - } + return computeValuesAndVectors(mat) + }, - // use dense 2D matrix implementation - const ans = checkAndSubmit(mat, size[0]) - return { values: ans[0], vectors: ans[1] } + 'Array, number|BigNumber': function (x, prec) { + const mat = matrix(x) + return computeValuesAndVectors(mat, prec) }, - Matrix: function (x) { - // use dense 2D array implementation - // dense matrix - const size = x.size() - if (size.length !== 2 || size[0] !== size[1]) { - throw new RangeError('Matrix must be square ' + - '(size: ' + format(size) + ')') - } - const ans = checkAndSubmit(x, size[0]) - return { values: matrix(ans[0]), vectors: matrix(ans[1]) } + Matrix: function (mat) { + return computeValuesAndVectors(mat) + }, + + 'Matrix, number|BigNumber': function (mat, prec) { + return computeValuesAndVectors(mat, prec) } }) - // Is the matrix - // symmetric ? - function isSymmetric (x, n) { - for (let i = 0; i < n; i++) { - for (let j = i; j < n; j++) { - // not symmtric - if (!equal(x[i][j], x[j][i])) { - throw new TypeError('Input matrix is not symmetric') - } - } + function computeValuesAndVectors (mat, prec) { + if (prec === undefined) { + prec = config.epsilon } - } - // check input for possible problems - // and perform diagonalization efficiently for - // specific type of number - function checkAndSubmit (x, n) { - let type = x.datatype() - // type check - if (type === undefined) { - type = x.getDataType() + const size = mat.size() + + if (size.length !== 2 || size[0] !== size[1]) { + throw new RangeError('Matrix must be square (size: ' + format(size) + ')') } - if (type !== 'number' && type !== 'BigNumber' && type !== 'Fraction') { - if (type === 'mixed') { - throw new TypeError('Mixed matrix element type is not supported') - } else { - throw new TypeError('Matrix element type not supported (' + type + ')') + + const arr = mat.toArray() + const N = size[0] + + if (isReal(arr, N, prec)) { + coerceReal(arr, N) + + if (isSymmetric(arr, N, prec)) { + const type = coerceTypes(mat, arr, N) + return doRealSymetric(arr, N, prec, type) } - } else { - isSymmetric(x.toArray(), n) } - // perform efficient calculation for 'numbers' - if (type === 'number') { - return diag(x.toArray()) - } else if (type === 'Fraction') { - const xArr = x.toArray() - // convert fraction to numbers - for (let i = 0; i < n; i++) { - for (let j = i; j < n; j++) { - xArr[i][j] = xArr[i][j].valueOf() - xArr[j][i] = xArr[i][j] + const type = coerceTypes(mat, arr, N) + return doComplex(arr, N, prec, type) + } + + /** @return {boolean} */ + function isSymmetric (arr, N, prec) { + for (let i = 0; i < N; i++) { + for (let j = i; j < N; j++) { + // TODO proper comparison of bignum and frac + if (larger(bignumber(abs(subtract(arr[i][j], arr[j][i]))), prec)) { + return false } } - return diag(x.toArray()) - } else if (type === 'BigNumber') { - return diagBig(x.toArray()) } + + return true } - // diagonalization implementation for number (efficient) - function diag (x) { - const N = x.length - const e0 = Math.abs(config.epsilon / N) - let psi - let Sij = new Array(N) - // Sij is Identity Matrix - for (let i = 0; i < N; i++) { - Sij[i] = createArray(N, 0) - Sij[i][i] = 1.0 - } - // initial error - let Vab = getAij(x) - while (Math.abs(Vab[1]) >= Math.abs(e0)) { - const i = Vab[0][0] - const j = Vab[0][1] - psi = getTheta(x[i][i], x[j][j], x[i][j]) - x = x1(x, psi, i, j) - Sij = Sij1(Sij, psi, i, j) - Vab = getAij(x) - } - const Ei = createArray(N, 0) // eigenvalues + /** @return {boolean} */ + function isReal (arr, N, prec) { for (let i = 0; i < N; i++) { - Ei[i] = x[i][i] + for (let j = 0; j < N; j++) { + // TODO proper comparison of bignum and frac + if (larger(bignumber(abs(im(arr[i][j]))), prec)) { + return false + } + } } - return sorting(clone(Ei), clone(Sij)) + + return true } - // diagonalization implementation for bigNumber - function diagBig (x) { - const N = x.length - const e0 = abs(config.epsilon / N) - let psi - let Sij = new Array(N) - // Sij is Identity Matrix - for (let i = 0; i < N; i++) { - Sij[i] = createArray(N, 0) - Sij[i][i] = 1.0 - } - // initial error - let Vab = getAijBig(x) - while (abs(Vab[1]) >= abs(e0)) { - const i = Vab[0][0] - const j = Vab[0][1] - psi = getThetaBig(x[i][i], x[j][j], x[i][j]) - x = x1Big(x, psi, i, j) - Sij = Sij1Big(Sij, psi, i, j) - Vab = getAijBig(x) - } - const Ei = createArray(N, 0) // eigenvalues + function coerceReal (arr, N) { for (let i = 0; i < N; i++) { - Ei[i] = x[i][i] + for (let j = 0; j < N; j++) { + arr[i][j] = re(arr[i][j]) + } } - // return [clone(Ei), clone(Sij)] - return sorting(clone(Ei), clone(Sij)) } - // get angle - function getTheta (aii, ajj, aij) { - const denom = (ajj - aii) - if (Math.abs(denom) <= config.epsilon) { - return Math.PI / 4 - } else { - return 0.5 * Math.atan(2 * aij / (ajj - aii)) - } - } + /** @return {'number' | 'BigNumber' | 'Complex'} */ + function coerceTypes (mat, arr, N) { + /** @type {string} */ + const type = mat.datatype() - // get angle - function getThetaBig (aii, ajj, aij) { - const denom = subtract(ajj, aii) - if (abs(denom) <= config.epsilon) { - return bignumber(-1).acos().div(4) - } else { - return multiplyScalar(0.5, atan(multiply(2, aij, inv(denom)))) + if (type === 'number' || type === 'BigNumber' || type === 'Complex') { + return type } - } - // update eigvec - function Sij1 (Sij, theta, i, j) { - const N = Sij.length - const c = Math.cos(theta) - const s = Math.sin(theta) - const Ski = createArray(N, 0) - const Skj = createArray(N, 0) - for (let k = 0; k < N; k++) { - Ski[k] = c * Sij[k][i] - s * Sij[k][j] - Skj[k] = s * Sij[k][i] + c * Sij[k][j] - } - for (let k = 0; k < N; k++) { - Sij[k][i] = Ski[k] - Sij[k][j] = Skj[k] - } - return Sij - } - // update eigvec for overlap - function Sij1Big (Sij, theta, i, j) { - const N = Sij.length - const c = cos(theta) - const s = sin(theta) - const Ski = createArray(N, bignumber(0)) - const Skj = createArray(N, bignumber(0)) - for (let k = 0; k < N; k++) { - Ski[k] = subtract(multiplyScalar(c, Sij[k][i]), multiplyScalar(s, Sij[k][j])) - Skj[k] = addScalar(multiplyScalar(s, Sij[k][i]), multiplyScalar(c, Sij[k][j])) - } - for (let k = 0; k < N; k++) { - Sij[k][i] = Ski[k] - Sij[k][j] = Skj[k] - } - return Sij - } + let hasNumber = false + let hasBig = false + let hasComplex = false - // update matrix - function x1Big (Hij, theta, i, j) { - const N = Hij.length - const c = bignumber(cos(theta)) - const s = bignumber(sin(theta)) - const c2 = multiplyScalar(c, c) - const s2 = multiplyScalar(s, s) - const Aki = createArray(N, bignumber(0)) - const Akj = createArray(N, bignumber(0)) - // 2cs Hij - const csHij = multiply(bignumber(2), c, s, Hij[i][j]) - // Aii - const Aii = addScalar(subtract(multiplyScalar(c2, Hij[i][i]), csHij), multiplyScalar(s2, Hij[j][j])) - const Ajj = add(multiplyScalar(s2, Hij[i][i]), csHij, multiplyScalar(c2, Hij[j][j])) - // 0 to i - for (let k = 0; k < N; k++) { - Aki[k] = subtract(multiplyScalar(c, Hij[i][k]), multiplyScalar(s, Hij[j][k])) - Akj[k] = addScalar(multiplyScalar(s, Hij[i][k]), multiplyScalar(c, Hij[j][k])) - } - // Modify Hij - Hij[i][i] = Aii - Hij[j][j] = Ajj - Hij[i][j] = bignumber(0) - Hij[j][i] = bignumber(0) - // 0 to i - for (let k = 0; k < N; k++) { - if (k !== i && k !== j) { - Hij[i][k] = Aki[k] - Hij[k][i] = Aki[k] - Hij[j][k] = Akj[k] - Hij[k][j] = Akj[k] + for (let i = 0; i < N; i++) { + for (let j = 0; j < N; j++) { + const el = arr[i][j] + + if (isNumber(el) || isFraction(el)) { + hasNumber = true + } else if (isBigNumber(el)) { + hasBig = true + } else if (isComplex(el)) { + hasComplex = true + } else { + throw TypeError('Unsupported type in Matrix: ' + typeOf(el)) + } } } - return Hij - } - // update matrix - function x1 (Hij, theta, i, j) { - const N = Hij.length - const c = Math.cos(theta) - const s = Math.sin(theta) - const c2 = c * c - const s2 = s * s - const Aki = createArray(N, 0) - const Akj = createArray(N, 0) - // Aii - const Aii = c2 * Hij[i][i] - 2 * c * s * Hij[i][j] + s2 * Hij[j][j] - const Ajj = s2 * Hij[i][i] + 2 * c * s * Hij[i][j] + c2 * Hij[j][j] - // 0 to i - for (let k = 0; k < N; k++) { - Aki[k] = c * Hij[i][k] - s * Hij[j][k] - Akj[k] = s * Hij[i][k] + c * Hij[j][k] - } - // Modify Hij - Hij[i][i] = Aii - Hij[j][j] = Ajj - Hij[i][j] = 0 - Hij[j][i] = 0 - // 0 to i - for (let k = 0; k < N; k++) { - if (k !== i && k !== j) { - Hij[i][k] = Aki[k] - Hij[k][i] = Aki[k] - Hij[j][k] = Akj[k] - Hij[k][j] = Akj[k] - } + if (hasBig && hasComplex) { + console.warn('Complex BigNumbers not supported, this operation will lose precission.') } - return Hij - } - // get max off-diagonal value from Upper Diagonal - function getAij (Mij) { - const N = Mij.length - let maxMij = 0 - let maxIJ = [0, 1] - for (let i = 0; i < N; i++) { - for (let j = i + 1; j < N; j++) { - if (Math.abs(maxMij) < Math.abs(Mij[i][j])) { - maxMij = Math.abs(Mij[i][j]) - maxIJ = [i, j] + if (hasComplex) { + for (let i = 0; i < N; i++) { + for (let j = 0; j < N; j++) { + arr[i][j] = complex(arr[i][j]) } } + + return 'Complex' } - return [maxIJ, maxMij] - } - // get max off-diagonal value from Upper Diagonal - function getAijBig (Mij) { - const N = Mij.length - let maxMij = 0 - let maxIJ = [0, 1] - for (let i = 0; i < N; i++) { - for (let j = i + 1; j < N; j++) { - if (abs(maxMij) < abs(Mij[i][j])) { - maxMij = abs(Mij[i][j]) - maxIJ = [i, j] + if (hasBig) { + for (let i = 0; i < N; i++) { + for (let j = 0; j < N; j++) { + arr[i][j] = bignumber(arr[i][j]) } } - } - return [maxIJ, maxMij] - } - // sort results - function sorting (E, S) { - const N = E.length - const Ef = Array(N) - const Sf = Array(N) - for (let k = 0; k < N; k++) { - Sf[k] = Array(N) + return 'BigNumber' } - for (let i = 0; i < N; i++) { - let minID = 0 - let minE = E[0] - for (let j = 0; j < E.length; j++) { - if (E[j] < minE) { - minID = j - minE = E[minID] + + if (hasNumber) { + for (let i = 0; i < N; i++) { + for (let j = 0; j < N; j++) { + arr[i][j] = number(arr[i][j]) } } - Ef[i] = E.splice(minID, 1)[0] - for (let k = 0; k < N; k++) { - Sf[k][i] = S[k][minID] - S[k].splice(minID, 1) - } - } - return [clone(Ef), clone(Sf)] - } - - /** - * Create an array of a certain size and fill all items with an initial value - * @param {number} size - * @param {number} value - * @return {number[]} - */ - function createArray (size, value) { - // TODO: as soon as all browsers support Array.fill, use that instead (IE doesn't support it) - const array = new Array(size) - for (let i = 0; i < size; i++) { - array[i] = value + return 'number' + } else { + throw TypeError('Matrix contains unsupported types only.') } - - return array } }) diff --git a/src/function/matrix/eigs/complex.js b/src/function/matrix/eigs/complex.js new file mode 100644 index 0000000000..c1eb9c5c02 --- /dev/null +++ b/src/function/matrix/eigs/complex.js @@ -0,0 +1,576 @@ +import { clone } from '../../../utils/object.js' + +export function createComplex ({ addScalar, subtract, flatten, multiply, multiplyScalar, divideScalar, sqrt, abs, bignumber, diag, inv, qr, usolveAll, equal, complex, larger, smaller, round, log10, transpose }) { + /** + * @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? + */ + function main (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 + const R = balance(arr, N, prec, type, findVectors) + + // R is the row transformation matrix + // A' = R A R⁻¹, A is the original matrix + // (if findVectors is false, R is undefined) + + // 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 + reduceToHessenberg(arr, N, prec, type, findVectors, R) + + // find eigenvalues + let { values, C } = iterateUntilTriangular(arr, N, prec, type, findVectors) + + // values is the list of eigenvalues, C is the column + // transformation matrix that transforms the hessenberg + // matrix to upper triangular + + // compose transformations A → hess. and hess. → triang. + C = multiply(inv(R), C) + + let vectors + + if (findVectors) { + vectors = findEigenvectors(arr, N, C, values, prec) + vectors = transpose((vectors)) // vectors are columns of a matrix + } + + 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) { + const big = type === 'BigNumber' + const cplx = type === 'Complex' + + const zero = big ? bignumber(0) : cplx ? complex(0) : 0 + const one = big ? bignumber(1) : cplx ? complex(1) : 1 + + // base of the floating-point arithmetic + const radix = big ? bignumber(10) : 2 + const radixSq = multiplyScalar(radix, radix) + + // the diagonal transformation matrix R + let Rdiag + if (findVectors) { + Rdiag = Array(N).fill(one) + } + + // this isn't the only time we loop thru the matrix... + let last = false + + while (!last) { + // ...haha I'm joking! unless... + last = true + + for (let i = 0; i < N; i++) { + // compute the taxicab norm of i-th column and row + // TODO optimize for complex numbers + let colNorm = zero + let rowNorm = zero + + for (let j = 0; j < N; j++) { + if (i === j) continue + const c = abs(arr[i][j]) + 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) + + let f = one + let c = colNorm + + const rowDivRadix = divideScalar(rowNorm, radix) + const 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) + const 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 + + const g = divideScalar(1, f) + + for (let 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) { + const big = type === 'BigNumber' + const cplx = type === 'Complex' + + const zero = big ? bignumber(0) : cplx ? complex(0) : 0 + + if (big) { prec = bignumber(prec) } + + for (let i = 0; i < N - 2; i++) { + // Find the largest subdiag element in the i-th col + + let maxIndex = 0 + let max = zero + + for (let j = i + 1; j < N; j++) { + const 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 + const tmp1 = arr[maxIndex] + arr[maxIndex] = arr[i + 1] + arr[i + 1] = tmp1 + + // Interchange maxIndex-th and (i+1)-th column + for (let j = 0; j < N; j++) { + const tmp2 = arr[j][maxIndex] + arr[j][maxIndex] = arr[j][i + 1] + arr[j][i + 1] = tmp2 + } + + // keep track of transformations + if (findVectors) { + const tmp3 = R[maxIndex] + R[maxIndex] = R[i + 1] + R[i + 1] = tmp3 + } + } + + // Reduce following rows and columns + for (let j = i + 2; j < N; j++) { + const n = divideScalar(arr[j][i], max) + + if (n === 0) { + continue + } + + // from j-th row subtract n-times (i+1)th row + for (let k = 0; k < N; k++) { + arr[j][k] = subtract(arr[j][k], multiplyScalar(n, arr[i + 1][k])) + } + + // to (i+1)th column add n-times j-th column + for (let k = 0; k < N; k++) { + arr[k][i + 1] = addScalar(arr[k][i + 1], multiplyScalar(n, arr[k][j])) + } + + // keep track of transformations + if (findVectors) { + for (let k = 0; k < N; k++) { + R[j][k] = subtract(R[j][k], multiplyScalar(n, R[i + 1][k])) + } + } + } + } + + 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) { + const big = type === 'BigNumber' + const cplx = type === 'Complex' + + const 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. + + let arr = clone(A) + + // the list of converged eigenvalues + const lambdas = [] + + // size of arr, which will get smaller as eigenvalues converge + let n = N + + // the diagonal of the block-diagonal matrix that turns + // converged 2x2 matrices into upper triangular matrices + const Sdiag = [] + + // N×N matrix describing the overall transformation done during the QR algorithm + let Qtotal = findVectors ? diag(Array(N).fill(one)) : undefined + + // n×n matrix describing the QR transformations done since last convergence + let Qpartial = findVectors ? diag(Array(n).fill(one)) : undefined + + // last eigenvalue converged before this many steps + let lastConvergenceBefore = 0 + + while (lastConvergenceBefore <= 100) { + lastConvergenceBefore += 1 + + // TODO if the convergence is slow, do something clever + + // Perform the factorization + + const k = 0 // TODO set close to an eigenvalue + + for (let i = 0; i < n; i++) { + arr[i][i] = subtract(arr[i][i], k) + } + + // TODO do an implicit QR transformation + const { Q, R } = qr(arr) + arr = multiply(R, Q) + + for (let 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 (let i = 0; i < n; i++) { + arr[i].pop() + } + + // The rightmost diagonal 2x2 block converged + } else if (n === 2 || smaller(abs(arr[n - 2][n - 3]), prec)) { + lastConvergenceBefore = 0 + const 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 (let i = 0; i < n; i++) { + arr[i].pop() + arr[i].pop() + } + } + + if (n === 0) { + break + } + } + + // standard sorting + lambdas.sort((a, b) => +subtract(abs(a), abs(b))) + + // the algorithm didn't converge + if (lastConvergenceBefore > 100) { + const 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 + const C = findVectors ? multiply(Qtotal, blockDiag(Sdiag, N)) : undefined + + return { values: lambdas, C } + } + + /** + * @param {Matrix} A original matrix + * @param {number} N size of A + * @param {Matrix} C column transformation matrix that turns A into upper triangular + * @param {number[]} values array of eigenvalues of A + * @returns {Matrix[]} eigenvalues + */ + function findEigenvectors (A, N, C, values, prec) { + const Cinv = inv(C) + const U = multiply(Cinv, A, C) + + // turn values into a kind of "multiset" + // this way it is easier to find eigenvectors + const uniqueValues = [] + const multiplicities = [] + + for (const λ of values) { + const i = indexOf(uniqueValues, λ, equal) + + // a dirty trick that helps us find more vectors + // TODO with iterative algorithm this can be removed + const roundedλ = round(λ, subtract(-1, log10(prec))) + + if (i === -1) { + uniqueValues.push(roundedλ) + 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) + + const vectors = [] + const len = uniqueValues.length + const b = Array(N).fill(0) + const E = diag(Array(N).fill(1)) + + // eigenvalues for which usolve failed (due to numerical error) + const failedLambdas = [] + + for (let i = 0; i < len; i++) { + const λ = uniqueValues[i] + + let solutions = usolveAll(subtract(U, multiply(λ, E)), b) + solutions = solutions.map(v => multiply(C, v)) + + solutions.shift() // ignore the null vector + + // looks like we missed something + if (solutions.length < multiplicities[i]) { + failedLambdas.push(λ) + } + + vectors.push(...solutions.map(v => flatten(v))) + } + + if (failedLambdas.length !== 0) { + const 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 ) + const trA = addScalar(a, d) + const detA = subtract(multiplyScalar(a, d), multiplyScalar(b, c)) + const x = multiplyScalar(trA, 0.5) + const 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) { + const big = type === 'BigNumber' + const cplx = type === 'Complex' + + const zero = big ? bignumber(0) : cplx ? complex(0) : 0 + const 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⃗₂ ) + + const na = subtract(a, l1) + const nb = subtract(b, l1) + const nc = subtract(c, l1) + const 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 (let i = 0; i < arr.length; i++) { + arr[i].push(...Array(N - arr[i].length).fill(0)) + } + + // add rows + for (let i = arr.length; i < N; i++) { + arr.push(Array(N).fill(0)) + arr[i][i] = 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) { + const M = [] + for (let i = 0; i < N; i++) { + M[i] = Array(N).fill(0) + } + + let I = 0 + for (const sub of arr) { + const n = sub.length + + for (let i = 0; i < n; i++) { + for (let j = 0; j < n; j++) { + M[I + i][I + j] = sub[i][j] + } + } + + I += n + } + + return M + } + + /** + * Finds the index of an element in an array using a custom equality function + * @template T + * @param {Array} 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 (let i = 0; i < arr.length; i++) { + if (fn(arr[i], el)) { + return i + } + } + return -1 + } + + return main +} diff --git a/src/function/matrix/eigs/realSymetric.js b/src/function/matrix/eigs/realSymetric.js new file mode 100644 index 0000000000..57c215f2c6 --- /dev/null +++ b/src/function/matrix/eigs/realSymetric.js @@ -0,0 +1,282 @@ +import { clone } from '../../../utils/object.js' + +export function createRealSymmetric ({ config, addScalar, subtract, abs, atan, cos, sin, multiplyScalar, inv, bignumber, multiply, add }) { + /** + * @param {number[] | BigNumber[]} arr + * @param {number} N + * @param {number} prec + * @param {'number' | 'BigNumber'} type + */ + function main (arr, N, prec = config.epsilon, type) { + if (type === 'number') { + return diag(arr, prec) + } + + if (type === 'BigNumber') { + return diagBig(arr, prec) + } + + throw TypeError('Unsupported data type: ' + type) + } + + // diagonalization implementation for number (efficient) + function diag (x, precision) { + const N = x.length + const e0 = Math.abs(precision / N) + let psi + let Sij = new Array(N) + // Sij is Identity Matrix + for (let i = 0; i < N; i++) { + Sij[i] = createArray(N, 0) + Sij[i][i] = 1.0 + } + // initial error + let Vab = getAij(x) + while (Math.abs(Vab[1]) >= Math.abs(e0)) { + const i = Vab[0][0] + const j = Vab[0][1] + psi = getTheta(x[i][i], x[j][j], x[i][j]) + x = x1(x, psi, i, j) + Sij = Sij1(Sij, psi, i, j) + Vab = getAij(x) + } + const Ei = createArray(N, 0) // eigenvalues + for (let i = 0; i < N; i++) { + Ei[i] = x[i][i] + } + return sorting(clone(Ei), clone(Sij)) + } + + // diagonalization implementation for bigNumber + function diagBig (x, precision) { + const N = x.length + const e0 = abs(precision / N) + let psi + let Sij = new Array(N) + // Sij is Identity Matrix + for (let i = 0; i < N; i++) { + Sij[i] = createArray(N, 0) + Sij[i][i] = 1.0 + } + // initial error + let Vab = getAijBig(x) + while (abs(Vab[1]) >= abs(e0)) { + const i = Vab[0][0] + const j = Vab[0][1] + psi = getThetaBig(x[i][i], x[j][j], x[i][j]) + x = x1Big(x, psi, i, j) + Sij = Sij1Big(Sij, psi, i, j) + Vab = getAijBig(x) + } + const Ei = createArray(N, 0) // eigenvalues + for (let i = 0; i < N; i++) { + Ei[i] = x[i][i] + } + // return [clone(Ei), clone(Sij)] + return sorting(clone(Ei), clone(Sij)) + } + + // get angle + function getTheta (aii, ajj, aij) { + const denom = (ajj - aii) + if (Math.abs(denom) <= config.epsilon) { + return Math.PI / 4.0 + } else { + return 0.5 * Math.atan(2.0 * aij / (ajj - aii)) + } + } + + // get angle + function getThetaBig (aii, ajj, aij) { + const denom = subtract(ajj, aii) + if (abs(denom) <= config.epsilon) { + return bignumber(-1).acos().div(4) + } else { + return multiplyScalar(0.5, atan(multiply(2.0, aij, inv(denom)))) + } + } + + // update eigvec + function Sij1 (Sij, theta, i, j) { + const N = Sij.length + const c = Math.cos(theta) + const s = Math.sin(theta) + const Ski = createArray(N, 0) + const Skj = createArray(N, 0) + for (let k = 0; k < N; k++) { + Ski[k] = c * Sij[k][i] - s * Sij[k][j] + Skj[k] = s * Sij[k][i] + c * Sij[k][j] + } + for (let k = 0; k < N; k++) { + Sij[k][i] = Ski[k] + Sij[k][j] = Skj[k] + } + return Sij + } + // update eigvec for overlap + function Sij1Big (Sij, theta, i, j) { + const N = Sij.length + const c = cos(theta) + const s = sin(theta) + const Ski = createArray(N, bignumber(0)) + const Skj = createArray(N, bignumber(0)) + for (let k = 0; k < N; k++) { + Ski[k] = subtract(multiplyScalar(c, Sij[k][i]), multiplyScalar(s, Sij[k][j])) + Skj[k] = addScalar(multiplyScalar(s, Sij[k][i]), multiplyScalar(c, Sij[k][j])) + } + for (let k = 0; k < N; k++) { + Sij[k][i] = Ski[k] + Sij[k][j] = Skj[k] + } + return Sij + } + + // update matrix + function x1Big (Hij, theta, i, j) { + const N = Hij.length + const c = bignumber(cos(theta)) + const s = bignumber(sin(theta)) + const c2 = multiplyScalar(c, c) + const s2 = multiplyScalar(s, s) + const Aki = createArray(N, bignumber(0)) + const Akj = createArray(N, bignumber(0)) + // 2cs Hij + const csHij = multiply(bignumber(2), c, s, Hij[i][j]) + // Aii + const Aii = addScalar(subtract(multiplyScalar(c2, Hij[i][i]), csHij), multiplyScalar(s2, Hij[j][j])) + const Ajj = add(multiplyScalar(s2, Hij[i][i]), csHij, multiplyScalar(c2, Hij[j][j])) + // 0 to i + for (let k = 0; k < N; k++) { + Aki[k] = subtract(multiplyScalar(c, Hij[i][k]), multiplyScalar(s, Hij[j][k])) + Akj[k] = addScalar(multiplyScalar(s, Hij[i][k]), multiplyScalar(c, Hij[j][k])) + } + // Modify Hij + Hij[i][i] = Aii + Hij[j][j] = Ajj + Hij[i][j] = bignumber(0) + Hij[j][i] = bignumber(0) + // 0 to i + for (let k = 0; k < N; k++) { + if (k !== i && k !== j) { + Hij[i][k] = Aki[k] + Hij[k][i] = Aki[k] + Hij[j][k] = Akj[k] + Hij[k][j] = Akj[k] + } + } + return Hij + } + + // update matrix + function x1 (Hij, theta, i, j) { + const N = Hij.length + const c = Math.cos(theta) + const s = Math.sin(theta) + const c2 = c * c + const s2 = s * s + const Aki = createArray(N, 0) + const Akj = createArray(N, 0) + // Aii + const Aii = c2 * Hij[i][i] - 2 * c * s * Hij[i][j] + s2 * Hij[j][j] + const Ajj = s2 * Hij[i][i] + 2 * c * s * Hij[i][j] + c2 * Hij[j][j] + // 0 to i + for (let k = 0; k < N; k++) { + Aki[k] = c * Hij[i][k] - s * Hij[j][k] + Akj[k] = s * Hij[i][k] + c * Hij[j][k] + } + // Modify Hij + Hij[i][i] = Aii + Hij[j][j] = Ajj + Hij[i][j] = 0 + Hij[j][i] = 0 + // 0 to i + for (let k = 0; k < N; k++) { + if (k !== i && k !== j) { + Hij[i][k] = Aki[k] + Hij[k][i] = Aki[k] + Hij[j][k] = Akj[k] + Hij[k][j] = Akj[k] + } + } + return Hij + } + + // get max off-diagonal value from Upper Diagonal + function getAij (Mij) { + const N = Mij.length + let maxMij = 0 + let maxIJ = [0, 1] + for (let i = 0; i < N; i++) { + for (let j = i + 1; j < N; j++) { + if (Math.abs(maxMij) < Math.abs(Mij[i][j])) { + maxMij = Math.abs(Mij[i][j]) + maxIJ = [i, j] + } + } + } + return [maxIJ, maxMij] + } + + // get max off-diagonal value from Upper Diagonal + function getAijBig (Mij) { + const N = Mij.length + let maxMij = 0 + let maxIJ = [0, 1] + for (let i = 0; i < N; i++) { + for (let j = i + 1; j < N; j++) { + if (abs(maxMij) < abs(Mij[i][j])) { + maxMij = abs(Mij[i][j]) + maxIJ = [i, j] + } + } + } + return [maxIJ, maxMij] + } + + // sort results + function sorting (E, S) { + const N = E.length + const values = Array(N) + const vectors = Array(N) + + for (let k = 0; k < N; k++) { + vectors[k] = Array(N) + } + for (let i = 0; i < N; i++) { + let minID = 0 + let minE = E[0] + for (let j = 0; j < E.length; j++) { + if (abs(E[j]) < abs(minE)) { + minID = j + minE = E[minID] + } + } + values[i] = E.splice(minID, 1)[0] + for (let k = 0; k < N; k++) { + vectors[k][i] = S[k][minID] + S[k].splice(minID, 1) + } + } + + return { values, vectors } + } + + /** + * Create an array of a certain size and fill all items with an initial value + * @param {number} size + * @param {number} value + * @return {number[]} + */ + function createArray (size, value) { + // TODO: as soon as all browsers support Array.fill, use that instead (IE doesn't support it) + const array = new Array(size) + + for (let i = 0; i < size; i++) { + array[i] = value + } + + return array + } + + return main +} diff --git a/src/utils/array.js b/src/utils/array.js index 0a275b4955..cd434ab63b 100644 --- a/src/utils/array.js +++ b/src/utils/array.js @@ -551,7 +551,7 @@ export function generalize (a) { * This method does not validate Array Matrix shape * @param {Array} array * @param {function} typeOf Callback function to use to determine the type of a value - * @return string + * @return {string} */ export function getArrayDataType (array, typeOf) { let type // to hold type info diff --git a/test/unit-tests/function/matrix/eigs.test.js b/test/unit-tests/function/matrix/eigs.test.js index e100765ba8..84846100e6 100644 --- a/test/unit-tests/function/matrix/eigs.test.js +++ b/test/unit-tests/function/matrix/eigs.test.js @@ -2,22 +2,20 @@ import assert from 'assert' import math from '../../../../src/defaultInstance.js' import approx from '../../../../tools/approx.js' const eigs = math.eigs +const complex = math.complex describe('eigs', function () { - it('should only accept a square matrix', function () { + it('only accepts a square matrix', function () { assert.throws(function () { eigs(math.matrix([[1, 2, 3], [4, 5, 6]])) }, /Matrix must be square/) assert.throws(function () { eigs([[1, 2, 3], [4, 5, 6]]) }, /Matrix must be square/) assert.throws(function () { eigs([[1, 2], [4, 5, 6]]) }, /DimensionError: Dimension mismatch/) assert.throws(function () { eigs([4, 5, 6]) }, /Matrix must be square/) assert.throws(function () { eigs(1.0) }, /TypeError: Unexpected type of argument/) assert.throws(function () { eigs('random') }, /TypeError: Unexpected type of argument/) - assert.throws(function () { eigs(math.matrix([[1, 2], [2.1, 3]])) }, /Input matrix is not symmetric/) }) - it('should only accept a matrix with valid element type', function () { - assert.throws(function () { eigs([['x', 2], [4, 5]]) }, /Mixed matrix element type is not supported/) - assert.throws(function () { eigs([[1, 2], [2, '5']]) }, /Mixed matrix element type is not supported/) - assert.throws(function () { eigs([['1', '2'], ['2', '5']]) }, /Matrix element type not supported/) + it('only accepts a matrix with valid element type', function () { + assert.throws(function () { eigs([['x', 2], [4, 5]]) }, /Cannot convert "x" to a number/) }) it('eigenvalue check for diagonal matrix', function () { @@ -28,22 +26,25 @@ describe('eigs', function () { approx.deepEqual(eigs( [[2, 0, 0], [0, 1, 0], [0, 0, 5]]).values, [1, 2, 5] ) + approx.deepEqual(eigs( + [[complex(2, 1), 0, 0], [0, 1, 0], [0, 0, complex(0, 5)]]).values, [complex(1, 0), complex(2, 1), complex(0, 5)] + ) }) - it('eigenvalue check for 2x2 simple matrix', function () { + it('calculates eigenvalues for 2x2 simple matrix', function () { // 2x2 test approx.deepEqual(eigs( [[1, 0.1], [0.1, 1]]).values, [0.9, 1.1] ) approx.deepEqual(eigs( - math.matrix([[1, 0.1], [0.1, 1]])).values, math.matrix([0.9, 1.1]) + math.matrix([[1, 0.1], [0.1, 1]])).values, [0.9, 1.1] ) approx.deepEqual(eigs( [[5, 2.3], [2.3, 1]]).values, [-0.04795013082563382, 6.047950130825635] ) }) - it('eigenvalue check for 3x3 and 4x4 matrix', function () { + it('calculates eigenvalues for 3x3 and 4x4 matrix', function () { // 3x3 test and 4x4 approx.deepEqual(eigs( [[1.0, 1.0, 1.0], @@ -56,7 +57,7 @@ describe('eigs', function () { [-3.8571699139231796, 0.7047577966772156, 0.9122549659760404, 0.9232933211541949], [2.852995822026198, 0.9122549659760404, 1.6598316026960402, -1.2931270747054358], [4.1957619745869845, 0.9232933211541949, -1.2931270747054358, -4.665994662426116]]).values, - [-8.687249803623432, -0.9135495807127523, 2.26552473288741, 5.6502090685149735] + [-0.9135495807127523, 2.26552473288741, 5.6502090685149735, -8.687249803623432] ) }) @@ -78,7 +79,7 @@ describe('eigs', function () { approx.deepEqual(Ei, E) }) - it('fractions are supported', function () { + it('supports fractions', function () { const aij = math.fraction('1/2') approx.deepEqual(eigs( [[aij, aij, aij], @@ -88,7 +89,7 @@ describe('eigs', function () { ) }) - it('bigNumber diagonalization is supported', function () { + it('diagonalizes matrix with bigNumber', function () { const x = [[math.bignumber(1), math.bignumber(0)], [math.bignumber(0), math.bignumber(1)]] approx.deepEqual(eigs(x).values, [math.bignumber(1), math.bignumber(1)]) const y = [[math.bignumber(1), math.bignumber(1.0)], [math.bignumber(1.0), math.bignumber(1)]] @@ -112,7 +113,7 @@ describe('eigs', function () { approx.deepEqual(Ei, E) }) - it('make sure BigNumbers input is actually calculated with BigNumber precision', function () { + it('actually calculates BigNumbers input with BigNumber precision', function () { const B = math.bignumber([ [0, 1], [1, 0]