From d5b1d25dfec0bd18513ef52ffb91425a5ebc945b Mon Sep 17 00:00:00 2001 From: David Sehnal <david.sehnal@gmail.com> Date: Tue, 21 May 2019 10:56:29 +0200 Subject: [PATCH] mol-math: added EVD code --- src/mol-math/linear-algebra/matrix/evd.ts | 311 +++++++++++++++++++ src/mol-math/linear-algebra/matrix/matrix.ts | 11 + 2 files changed, 322 insertions(+) create mode 100644 src/mol-math/linear-algebra/matrix/evd.ts diff --git a/src/mol-math/linear-algebra/matrix/evd.ts b/src/mol-math/linear-algebra/matrix/evd.ts new file mode 100644 index 000000000..789d314e6 --- /dev/null +++ b/src/mol-math/linear-algebra/matrix/evd.ts @@ -0,0 +1,311 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import Matrix from './matrix'; + +export namespace EVD { + export interface Cache { + size: number, + matrix: Matrix, + eigenValues: number[], + D: number[], + E: number[] + } + + export function createCache(size: number): Cache { + return { + size, + matrix: Matrix.create(size, size), + eigenValues: <any>new Float64Array(size), + D: <any>new Float64Array(size), + E: <any>new Float64Array(size) + } + } + + /** + * Computes EVD and stores the result in the cache. + */ + export function compute(cache: Cache): void { + symmetricEigenDecomp(cache.size, cache.matrix.data as number[], cache.eigenValues, cache.D, cache.E); + } +} + +// The EVD code has been adapted from Math.NET, MIT license, Copyright (c) 2002-2015 Math.NET +// +// THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +function symmetricEigenDecomp(order: number, matrixEv: number[], vectorEv: number[], d: number[], e: number[]) { + for (let i = 0; i < order; i++) { + e[i] = 0.0; + } + + let om1 = order - 1; + for (let i = 0; i < order; i++) { + d[i] = matrixEv[i * order + om1]; + } + + symmetricTridiagonalize(matrixEv, d, e, order); + symmetricDiagonalize(matrixEv, d, e, order); + + for (let i = 0; i < order; i++) { + vectorEv[i] = d[i]; + } +} + +function symmetricTridiagonalize(a: number[], d: number[], e: number[], order: number) { + // Householder reduction to tridiagonal form. + for (let i = order - 1; i > 0; i--) { + // Scale to avoid under/overflow. + let scale = 0.0; + let h = 0.0; + + for (let k = 0; k < i; k++) { + scale = scale + Math.abs(d[k]); + } + + if (scale === 0.0) { + e[i] = d[i - 1]; + for (let j = 0; j < i; j++) { + d[j] = a[(j * order) + i - 1]; + a[(j * order) + i] = 0.0; + a[(i * order) + j] = 0.0; + } + } + else { + // Generate Householder vector. + for (let k = 0; k < i; k++) { + d[k] /= scale; + h += d[k] * d[k]; + } + + let f = d[i - 1]; + let g = Math.sqrt(h); + if (f > 0) { + g = -g; + } + + e[i] = scale * g; + h = h - (f * g); + d[i - 1] = f - g; + + for (let j = 0; j < i; j++) { + e[j] = 0.0; + } + + // Apply similarity transformation to remaining columns. + for (let j = 0; j < i; j++) { + f = d[j]; + a[(i * order) + j] = f; + g = e[j] + (a[(j * order) + j] * f); + + for (let k = j + 1; k <= i - 1; k++) { + g += a[(j * order) + k] * d[k]; + e[k] += a[(j * order) + k] * f; + } + + e[j] = g; + } + + f = 0.0; + + for (let j = 0; j < i; j++) { + e[j] /= h; + f += e[j] * d[j]; + } + + let hh = f / (h + h); + + for (let j = 0; j < i; j++) { + e[j] -= hh * d[j]; + } + + for (let j = 0; j < i; j++) { + f = d[j]; + g = e[j]; + + for (let k = j; k <= i - 1; k++) { + a[(j * order) + k] -= (f * e[k]) + (g * d[k]); + } + + d[j] = a[(j * order) + i - 1]; + a[(j * order) + i] = 0.0; + } + } + + d[i] = h; + } + + // Accumulate transformations. + for (let i = 0; i < order - 1; i++) { + a[(i * order) + order - 1] = a[(i * order) + i]; + a[(i * order) + i] = 1.0; + let h = d[i + 1]; + if (h !== 0.0) { + for (let k = 0; k <= i; k++) { + d[k] = a[((i + 1) * order) + k] / h; + } + + for (let j = 0; j <= i; j++) { + let g = 0.0; + for (let k = 0; k <= i; k++) { + g += a[((i + 1) * order) + k] * a[(j * order) + k]; + } + + for (let k = 0; k <= i; k++) { + a[(j * order) + k] -= g * d[k]; + } + } + } + + for (let k = 0; k <= i; k++) { + a[((i + 1) * order) + k] = 0.0; + } + } + + for (let j = 0; j < order; j++) { + d[j] = a[(j * order) + order - 1]; + a[(j * order) + order - 1] = 0.0; + } + + a[(order * order) - 1] = 1.0; + e[0] = 0.0; +} + +function symmetricDiagonalize(a: number[], d: number[], e: number[], order: number) { + const maxiter = 1000; + + for (let i = 1; i < order; i++) { + e[i - 1] = e[i]; + } + + e[order - 1] = 0.0; + + let f = 0.0; + let tst1 = 0.0; + let eps = Math.pow(2, -53); // DoubleWidth = 53 + for (let l = 0; l < order; l++) { + // Find small subdiagonal element + tst1 = Math.max(tst1, Math.abs(d[l]) + Math.abs(e[l])); + let m = l; + while (m < order) { + if (Math.abs(e[m]) <= eps * tst1) { + break; + } + + m++; + } + + // If m == l, d[l] is an eigenvalue, + // otherwise, iterate. + if (m > l) { + let iter = 0; + do { + iter = iter + 1; // (Could check iteration count here.) + + // Compute implicit shift + let g = d[l]; + let p = (d[l + 1] - g) / (2.0 * e[l]); + let r = hypotenuse(p, 1.0); + if (p < 0) { + r = -r; + } + + d[l] = e[l] / (p + r); + d[l + 1] = e[l] * (p + r); + + let dl1 = d[l + 1]; + let h = g - d[l]; + for (let i = l + 2; i < order; i++) { + d[i] -= h; + } + + f = f + h; + + // Implicit QL transformation. + p = d[m]; + let c = 1.0; + let c2 = c; + let c3 = c; + let el1 = e[l + 1]; + let s = 0.0; + let s2 = 0.0; + for (let i = m - 1; i >= l; i--) { + c3 = c2; + c2 = c; + s2 = s; + g = c * e[i]; + h = c * p; + r = hypotenuse(p, e[i]); + e[i + 1] = s * r; + s = e[i] / r; + c = p / r; + p = (c * d[i]) - (s * g); + d[i + 1] = h + (s * ((c * g) + (s * d[i]))); + + // Accumulate transformation. + for (let k = 0; k < order; k++) { + h = a[((i + 1) * order) + k]; + a[((i + 1) * order) + k] = (s * a[(i * order) + k]) + (c * h); + a[(i * order) + k] = (c * a[(i * order) + k]) - (s * h); + } + } + + p = (-s) * s2 * c3 * el1 * e[l] / dl1; + e[l] = s * p; + d[l] = c * p; + + // Check for convergence. If too many iterations have been performed, + // throw exception that Convergence Failed + if (iter >= maxiter) { + throw 'SVD: Not converging.'; + } + } while (Math.abs(e[l]) > eps * tst1); + } + + d[l] = d[l] + f; + e[l] = 0.0; + } + + // Sort eigenvalues and corresponding vectors. + for (let i = 0; i < order - 1; i++) { + let k = i; + let p = d[i]; + for (let j = i + 1; j < order; j++) { + if (d[j] < p) { + k = j; + p = d[j]; + } + } + + if (k !== i) { + d[k] = d[i]; + d[i] = p; + for (let j = 0; j < order; j++) { + p = a[(i * order) + j]; + a[(i * order) + j] = a[(k * order) + j]; + a[(k * order) + j] = p; + } + } + } +} + +function hypotenuse(a: number, b: number) { + if (Math.abs(a) > Math.abs(b)) { + let r = b / a; + return Math.abs(a) * Math.sqrt(1 + (r * r)); + } + + if (b !== 0.0) { + let r = a / b; + return Math.abs(b) * Math.sqrt(1 + (r * r)); + } + + return 0.0; +} \ No newline at end of file diff --git a/src/mol-math/linear-algebra/matrix/matrix.ts b/src/mol-math/linear-algebra/matrix/matrix.ts index 0610a4648..737bfcad2 100644 --- a/src/mol-math/linear-algebra/matrix/matrix.ts +++ b/src/mol-math/linear-algebra/matrix/matrix.ts @@ -14,6 +14,17 @@ namespace Matrix { return { data: new ctor(size), size, cols, rows } } + /** Get element assuming data are stored in column-major order */ + export function get(m: Matrix, i: number, j: number) { return m.data[m.rows * j + i]; } + /** Set element assuming data are stored in column-major order */ + export function set(m: Matrix, i: number, j: number, value: number) { m.data[m.rows * j + i] = value; } + /** Add to element assuming data are stored in column-major order */ + export function add(m: Matrix, i: number, j: number, value: number) { m.data[m.rows * j + i] += value; } + /** Zero out the matrix */ + export function makeZero(m: Matrix) { + for (let i = 0, _l = m.data.length; i < _l; i++) m.data[i] = 0.0; + } + export function fromArray(data: NumberArray, cols: number, rows: number): Matrix { return { data, size: cols * rows, cols, rows } } -- GitLab