simple-squiggle

A restricted subset of Squiggle
Log | Files | Refs | README

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 }