├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2013 Eric Arnebäck 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cholesky-solve[WIP] 2 | 3 | This module solves sparse symmetric positive definite linear systems, 4 | by finding the Cholesky decomposition(the `LDL^T` decomposition, and not 5 | the `LL^T` decomposition), and then doing forward substitution and 6 | backward substitution. It is basically a Javascript port of the paper 7 | "Algorithm 8xx: a concise sparse Cholesky factorization package". This 8 | kind of solver has many applications in digital geometry processing. 9 | 10 | ## Install 11 | 12 | npm install cholesky-solve 13 | 14 | ## Example 15 | 16 | ```javascript 17 | var choleskySolve = require('cholesky-solve') 18 | 19 | // matrix dimension. 20 | const n = 10 21 | 22 | /* 23 | Below we specify the sparse matrix: 24 | 25 | 1.7 0 0 0 0 0 0 0 0.13 0 26 | 0 1.0 0 0 0.02 0 0 0 0 0.01 27 | 0 0 1.5 0 0 0 0 0 0 0 28 | 0 0 0 1.1 0 0 0 0 0 0 29 | 0 0.02 0 0 2.6 0 0.16 0.09 0.52 0.53 30 | 0 0 0 0 0 1.2 0 0 0 0 31 | 0 0 0 0 0.16 0 1.3 0 0 0.56 32 | 0 0 0 0 0.09 0 0 1.6 0.11 0 33 | 0.13 0 0 0 0.52 0 0 0.11 1.4 0 34 | 0 0.01 0 0 0.53 0 0.56 0 0 3.1 35 | 36 | Note that we only specify the coefficients on the diagonal, 37 | or above the diagonal. Since the matrix is symmetric, 38 | specifying the coefficients below the diagonal is completely redundant. 39 | Finally, the order in which the coefficients is specified in is not important. 40 | 41 | */ 42 | 43 | // the sparse matrix on left-hand side. 44 | var M = [ 45 | [2, 2, 1.5], 46 | [1, 1, 1.0], 47 | [1, 4, 0.02], 48 | [5, 5, 1.2], 49 | [7, 7, 1.6], 50 | [4, 4, 2.6], 51 | [3, 3, 1.1], 52 | [4, 7, 0.09], 53 | [4, 6, 0.16], 54 | [0, 0, 1.7], 55 | [4, 8, 0.52], 56 | [0, 8, 0.13], 57 | [6, 6, 1.3], 58 | [7, 8, 0.11], 59 | [4, 9, 0.53], 60 | [8, 8, 1.4], 61 | [9, 9, 3.1], 62 | [1, 9, 0.01], 63 | [6, 9, 0.56] 64 | ] 65 | 66 | // right-hand side 67 | var b = [0.287, 0.22, 0.45, 0.44, 2.486, 0.72, 1.55, 1.424, 1.621, 3.759] 68 | 69 | var P = require('cuthill-mckee')(M, n) 70 | 71 | // finally, solve the equation 72 | // Mx = b 73 | // and print x 74 | 75 | // the `prepare` method returns a function that can be used to solve 76 | // the equation for any value of b. 77 | var solve = choleskySolve.prepare(M, n, P) 78 | console.log(solve(b)) 79 | ``` 80 | 81 | ## API 82 | 83 | ### `require("cholesky-solve").prepare(M, n, [P])` 84 | 85 | Decomposes `M` into the Cholesky decomposition of the form `LDL^T`. A 86 | function is returned that can be used to solve the equation `Mx = b`, 87 | for some given value of `b`. 88 | 89 | * `M` a list of the matrix coefficients of the sparse matrix `M`. These are 90 | the coefficients on the diagonal and above the diagonal. The ones below the 91 | diagonal do not need to be specified, since the matrix is symmetric. 92 | * `n` the dimension of the matrix `M` 93 | * `P` encodes a permutation matrix that preconditions `M` before the Cholesky decomposition is solved for. A possible algorithm for finding a good permutation is 94 | [Cuthill–McKee](https://en.wikipedia.org/wiki/Cuthill%E2%80%93McKee_algorithm). See 95 | the module [cuthill-mckee](https://github.com/mikolalysenko/cuthill-mckee) for a 96 | Javascript implementation. 97 | 98 | **Returns** A function that takes a single argument `b`. The function 99 | returns the solution to the equation `Mx = b`, encoded as a simple array. 100 | 101 | **NOTE** the module does no sanity checking on the input arguments. It is assumed that the user knows what he/she is doing! 102 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | function ldl_symbolic 2 | ( 3 | n, /* A and L are n-by-n, where n >= 0 */ 4 | Ap, /* input of size n + 1, not modified */ 5 | Ai, /* input of size nz=Ap[n], not modified */ 6 | Lp, /* output of size n + 1, not defined on input */ 7 | Parent, /* output of size n, not defined on input */ 8 | Lnz, /* output of size n, not defined on input */ 9 | Flag /* workspace of size n, not defn. on input or output */ 10 | ) { 11 | 12 | var i, k, p, kk, p2 13 | 14 | for (k = 0; k < n; k++) { 15 | /* L(k,:) pattern: all nodes reachable in etree from nz in A(0:k-1,k) */ 16 | Parent[k] = -1 /* parent of k is not yet known */ 17 | Flag[k] = k /* mark node k as visited */ 18 | Lnz[k] = 0 /* count of nonzeros in column k of L */ 19 | kk = (k) /* kth original, or permuted, column */ 20 | p2 = Ap[kk + 1] 21 | for (p = Ap[kk]; p < p2; p++) { 22 | /* A (i,k) is nonzero (original or permuted A) */ 23 | i = (Ai[p]) 24 | 25 | if (i < k) { 26 | /* follow path from i to root of etree, stop at flagged node */ 27 | for (; Flag[i] !== k; i = Parent[i]) { 28 | /* find parent of i if not yet determined */ 29 | if (Parent[i] === -1) Parent[i] = k 30 | Lnz[i]++ /* L (k,i) is nonzero */ 31 | Flag[i] = k /* mark i as visited */ 32 | } 33 | } 34 | } 35 | } 36 | /* construct Lp index array from Lnz column counts */ 37 | Lp[0] = 0 38 | for (k = 0; k < n; k++) { 39 | Lp[k + 1] = Lp[k] + Lnz[k] 40 | } 41 | } 42 | 43 | function ldl_numeric /* returns n if successful, k if D (k,k) is zero */ 44 | ( 45 | n, /* A and L are n-by-n, where n >= 0 */ 46 | Ap, /* input of size n+1, not modified */ 47 | Ai, /* input of size nz=Ap[n], not modified */ 48 | Ax, /* input of size nz=Ap[n], not modified */ 49 | Lp, /* input of size n+1, not modified */ 50 | Parent, /* input of size n, not modified */ 51 | Lnz, /* output of size n, not defn. on input */ 52 | Li, /* output of size lnz=Lp[n], not defined on input */ 53 | Lx, /* output of size lnz=Lp[n], not defined on input */ 54 | D, /* output of size n, not defined on input */ 55 | Y, /* workspace of size n, not defn. on input or output */ 56 | Pattern, /* workspace of size n, not defn. on input or output */ 57 | Flag /* workspace of size n, not defn. on input or output */ 58 | ) { 59 | 60 | var yi, l_ki 61 | var i, k, p, kk, p2, len, top 62 | for (k = 0; k < n; k++) { 63 | /* compute nonzero Pattern of kth row of L, in topological order */ 64 | Y[k] = 0.0 /* Y(0:k) is now all zero */ 65 | top = n /* stack for pattern is empty */ 66 | Flag[k] = k /* mark node k as visited */ 67 | Lnz[k] = 0 /* count of nonzeros in column k of L */ 68 | kk = (k) /* kth original, or permuted, column */ 69 | p2 = Ap[kk + 1] 70 | for (p = Ap[kk]; p < p2; p++) { 71 | i = (Ai[p]) /* get A(i,k) */ 72 | if (i <= k) { 73 | Y[i] += Ax[p] /* scatter A(i,k) into Y (sum duplicates) */ 74 | for (len = 0; Flag[i] !== k; i = Parent[i]) { 75 | Pattern[len++] = i /* L(k,i) is nonzero */ 76 | Flag[i] = k /* mark i as visited */ 77 | } 78 | while (len > 0) Pattern[--top] = Pattern[--len] 79 | } 80 | } 81 | /* compute numerical values kth row of L (a sparse triangular solve) */ 82 | D[k] = Y[k] /* get D(k,k) and clear Y(k) */ 83 | Y[k] = 0.0 84 | for (; top < n; top++) { 85 | i = Pattern[top] /* Pattern[top:n-1] is pattern of L(:,k) */ 86 | yi = Y[i] /* get and clear Y(i) */ 87 | Y[i] = 0.0 88 | p2 = Lp[i] + Lnz[i] 89 | for (p = Lp[i]; p < p2; p++) { 90 | Y[Li[p]] -= Lx[p] * yi 91 | } 92 | l_ki = yi / D[i] /* the nonzero entry L(k,i) */ 93 | D[k] -= l_ki * yi 94 | Li[p] = k /* store L(k,i) in column form of L */ 95 | Lx[p] = l_ki 96 | Lnz[i]++ /* increment count of nonzeros in col i */ 97 | } 98 | 99 | if (D[k] === 0.0) return (k) /* failure, D(k,k) is zero */ 100 | } 101 | 102 | return (n) /* success, diagonal of D is all nonzero */ 103 | } 104 | 105 | function ldl_lsolve 106 | ( 107 | n, /* L is n-by-n, where n >= 0 */ 108 | X, /* size n. right-hand-side on input, soln. on output */ 109 | Lp, /* input of size n+1, not modified */ 110 | Li, /* input of size lnz=Lp[n], not modified */ 111 | Lx /* input of size lnz=Lp[n], not modified */ 112 | ) { 113 | var j, p, p2 114 | for (j = 0; j < n; j++) { 115 | p2 = Lp[j + 1] 116 | for (p = Lp[j]; p < p2; p++) { 117 | X[Li[p]] -= Lx[p] * X[j] 118 | } 119 | } 120 | } 121 | function ldl_dsolve 122 | ( 123 | n, /* D is n-by-n, where n >= 0 */ 124 | X, /* size n. right-hand-side on input, soln. on output */ 125 | D /* input of size n, not modified */ 126 | ) { 127 | var j 128 | for (j = 0; j < n; j++) { 129 | X[j] /= D[j] 130 | } 131 | } 132 | function ldl_ltsolve 133 | ( 134 | n, /* L is n-by-n, where n >= 0 */ 135 | X, /* size n. right-hand-side on input, soln. on output */ 136 | Lp, /* input of size n+1, not modified */ 137 | Li, /* input of size lnz=Lp[n], not modified */ 138 | Lx /* input of size lnz=Lp[n], not modified */ 139 | ) { 140 | var j, p, p2 141 | for (j = n - 1; j >= 0; j--) { 142 | p2 = Lp[j + 1] 143 | for (p = Lp[j]; p < p2; p++) { 144 | X[j] -= Lx[p] * X[Li[p]] 145 | } 146 | } 147 | } 148 | 149 | function ldl_perm 150 | ( 151 | n, /* size of X, B, and P */ 152 | X, /* output of size n. */ 153 | B, /* input of size n. */ 154 | P /* input permutation array of size n. */ 155 | ) { 156 | var j ; 157 | for (j = 0; j < n; j++) 158 | { 159 | X[j] = B [P[j]] 160 | } 161 | } 162 | 163 | function ldl_permt( 164 | n, /* size of X, B, and P */ 165 | X, /* output of size n. */ 166 | B, /* input of size n. */ 167 | P /* input permutation array of size n. */ 168 | ) { 169 | var j 170 | for (j = 0; j < n; j++) 171 | { 172 | X [P[j]] = B[j] 173 | } 174 | } 175 | 176 | function prepare (M, n, P) { 177 | const ANZ = M.length 178 | 179 | // if a permutation was specified, apply it. 180 | if (P) { 181 | var Pinv = new Array(n) 182 | 183 | for (k = 0; k < n; k++) { 184 | Pinv[P[k]] = k 185 | } 186 | 187 | var Mt = [] // scratch memory 188 | // Apply permutation. We make M into P*M*P^T 189 | for(var a = 0; a < M.length; ++a) { 190 | var ar = Pinv[M[a][0]] 191 | var ac = Pinv[M[a][1]] 192 | 193 | // we only store the upper-diagonal elements(since we assume matrix is symmetric, we only need to store these) 194 | // if permuted element is below diagonal, we simply transpose it. 195 | if(ac < ar) { 196 | var t = ac 197 | ac = ar 198 | ar = t 199 | } 200 | 201 | Mt[a] = [] 202 | Mt[a][0] = ar 203 | Mt[a][1] = ac 204 | Mt[a][2] = M[a][2] 205 | } 206 | 207 | M = Mt // copy scratch memory. 208 | } else { 209 | // if P argument is null, we just use an identity permutation. 210 | var P = [] 211 | for(var i = 0; i < n; ++i) { 212 | P[i] = i 213 | } 214 | } 215 | 216 | // The sparse matrix we are decomposing is A. 217 | // Now we shall create A from M. 218 | var Ap = new Array(n + 1) 219 | var Ai = new Array(M.length) 220 | var Ax = new Array(M.length) 221 | 222 | // count number of non-zero elements in columns. 223 | var LNZ = [] 224 | for(var i = 0; i < n; ++i) { 225 | LNZ[i] = 0 226 | } 227 | for(var a = 0; a < M.length; ++a) { 228 | LNZ[M[a][1]]++ 229 | } 230 | 231 | Ap[0] = 0 232 | for(var i = 0; i < n; ++i) { 233 | Ap[i+1] = Ap[i] + LNZ[i] 234 | } 235 | 236 | var coloffset = [] 237 | for(var a = 0; a < n; ++a) { 238 | coloffset[a] = 0 239 | } 240 | 241 | // go through all elements in M, and add them to sparse matrix A. 242 | for(var i = 0; i < M.length; ++i) { 243 | var e = M[i] 244 | var col = e[1] 245 | 246 | var adr = Ap[col] + coloffset[col] 247 | Ai[adr] = e[0] 248 | Ax[adr] = e[2] 249 | 250 | coloffset[col]++ 251 | } 252 | 253 | var D = new Array(n) 254 | var Y = new Array(n) 255 | var Lp = new Array(n + 1) 256 | var Parent = new Array(n) 257 | var Lnz = new Array(n) 258 | var Flag = new Array(n) 259 | var Pattern = new Array(n) 260 | var bp1 = new Array(n) 261 | var x = new Array(n) 262 | var d 263 | 264 | ldl_symbolic(n, Ap, Ai, Lp, Parent, Lnz, Flag) 265 | 266 | var Lx = new Array(Lp[n]) 267 | var Li = new Array(Lp[n]) 268 | 269 | d = ldl_numeric(n, Ap, Ai, Ax, Lp, Parent, Lnz, Li, Lx, D, Y, Pattern, Flag) 270 | 271 | if (d === n) { 272 | return function(b) { 273 | ldl_perm(n, bp1, b, P); 274 | ldl_lsolve(n, bp1, Lp, Li, Lx) 275 | ldl_dsolve(n, bp1, D) 276 | ldl_ltsolve(n, bp1, Lp, Li, Lx) 277 | ldl_permt(n, x, bp1, P); 278 | 279 | return x 280 | } 281 | 282 | } else { 283 | return null 284 | } 285 | } 286 | 287 | module.exports.prepare = prepare 288 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cholesky-solve", 3 | "version": "0.2.1", 4 | "description": "This module solves sparse symmetric positive definite linear systems by using the Cholesky decomposition", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "tape test.js" 8 | }, 9 | "devDependencies": { 10 | "almost-equal": "^1.1.0", 11 | "cuthill-mckee": "^1.0.0", 12 | "tape": "^4.7.0", 13 | "xorshift": "^1.1.0" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/Erkaman/cholesky-solve.git" 18 | }, 19 | "keywords": [ 20 | "numerical", 21 | "computing", 22 | "sparse", 23 | "matrix", 24 | "solver", 25 | "linear", 26 | "system", 27 | "cholesky" 28 | ], 29 | "author": "Eric Arnebäck", 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/Erkaman/cholesky-solve/issues" 33 | }, 34 | "homepage": "https://github.com/Erkaman/cholesky-solve" 35 | 36 | } 37 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var choleskySolve = require('./') 2 | var test = require('tape') 3 | var almostEqual = require("almost-equal") 4 | 5 | // deterministic RNG for generating test data. 6 | var rng = new require('xorshift').constructor([1, 0, 2, 0]); 7 | 8 | var eps = 1e-5 9 | 10 | function choleskySolveHelper(M, b, n, P) { 11 | var solve = choleskySolve.prepare(M, n, P) 12 | return solve(b) 13 | } 14 | 15 | function solveAndAssert(t, n, M, b, P, expectedSolution) { 16 | var foundSolution = choleskySolveHelper(M, b, n, P) 17 | 18 | for(var i=0; i< n; ++i) { 19 | t.assert(almostEqual(expectedSolution[i], foundSolution[i], eps, eps), "solution element " + i + ": "+ expectedSolution[i] + " = " + foundSolution[i]) 20 | } 21 | } 22 | 23 | test('solve10x10matrix', function(t) { 24 | const n = 10 25 | var M = [ 26 | [2, 2, 1.5], 27 | [1, 1, 1.0], 28 | [1, 4, 0.02], 29 | [5, 5, 1.2], 30 | [7, 7, 1.6], 31 | [4, 4, 2.6], 32 | [3, 3, 1.1], 33 | [4, 7, 0.09], 34 | [4, 6, 0.16], 35 | [0, 0, 1.7], 36 | [4, 8, 0.52], 37 | [0, 8, 0.13], 38 | [6, 6, 1.3], 39 | [7, 8, 0.11], 40 | [4, 9, 0.53], 41 | [8, 8, 1.4], 42 | [9, 9, 3.1], 43 | [1, 9, 0.01], 44 | [6, 9, 0.56] 45 | ] 46 | 47 | var b = [0.287, 0.22, 0.45, 0.44, 2.486, 0.72, 1.55, 1.424, 1.621, 3.759] 48 | 49 | var expectedSolution =[0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0] 50 | 51 | var P = null // identity permutation 52 | 53 | solveAndAssert(t, n, M, b, P, expectedSolution) 54 | 55 | t.end(); 56 | }) 57 | 58 | test('solve10x10matrix_cuthill_mckee', function(t) { 59 | const n = 10 60 | var M = [ 61 | [2, 2, 1.5], 62 | [1, 1, 1.0], 63 | [1, 4, 0.02], 64 | [5, 5, 1.2], 65 | [7, 7, 1.6], 66 | [4, 4, 2.6], 67 | [3, 3, 1.1], 68 | [4, 7, 0.09], 69 | [4, 6, 0.16], 70 | [0, 0, 1.7], 71 | [4, 8, 0.52], 72 | [0, 8, 0.13], 73 | [6, 6, 1.3], 74 | [7, 8, 0.11], 75 | [4, 9, 0.53], 76 | [8, 8, 1.4], 77 | [9, 9, 3.1], 78 | [1, 9, 0.01], 79 | [6, 9, 0.56] 80 | ] 81 | 82 | var b = [0.287, 0.22, 0.45, 0.44, 2.486, 0.72, 1.55, 1.424, 1.621, 3.759] 83 | 84 | var expectedSolution =[0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0] 85 | 86 | var P = require('cuthill-mckee')(M, n) 87 | 88 | solveAndAssert(t, n, M, b, P, expectedSolution) 89 | 90 | t.end(); 91 | }) 92 | 93 | test('solve1000x1000matrix', function(t) { 94 | var L = [] 95 | var n = 1000 96 | 97 | // first generate a lower-triangular matrix L 98 | for(var i = 0; i < n; ++i) { 99 | L[i] = [] 100 | 101 | for(var j = 0; j 0.9 || i === j) { 107 | L[i][j] = Math.floor(rng.random() * 10)+1 108 | } else { 109 | L[i][j] = 0 110 | } 111 | } 112 | } 113 | 114 | // next, we simply multiply L with its transpose, and put the result in A. 115 | // the resulting matrix is symmetric, and positive definite, 116 | // thus it must have a cholesky decomposition. 117 | var A = [] 118 | for(var i = 0; i < n; ++i) { 119 | A[i] = [] 120 | } 121 | for(var i = 0; i < n; ++i) { 122 | for(var j = 0; j < n; ++j) { 123 | var s = 0.0 124 | for(var k = 0; k < n; ++k) { 125 | s += L[i][k] * L[j][k] 126 | } 127 | A[i][j] = s 128 | } 129 | } 130 | 131 | // now store A as a sparse matrix M. 132 | var M = [] 133 | for(var row = 0; row < n; ++row) { 134 | for(var col = row; col < n; ++col) { 135 | // console.log(row, col) 136 | if(A[row][col] > 0.0001) { 137 | M.push([row, col, A[row][col]]) 138 | } 139 | } 140 | } 141 | 142 | // In our test, we shall solve the equation 143 | // Mx = b 144 | // so randomly generate x. 145 | var x = [] 146 | var b = [] 147 | for(var i = 0; i < n; ++i) { 148 | x[i] = Math.floor(rng.random() * 9) 149 | } 150 | 151 | // Now compute b as b = Mx 152 | for(var i = 0; i < n; ++i) { 153 | var s = 0.0 154 | for(var k = 0; k < n; ++k) { 155 | s += A[i][k] * x[k] 156 | } 157 | b[i] = s 158 | } 159 | 160 | 161 | var P = require('cuthill-mckee')(M, n) 162 | // solve. 163 | var foundSolution = choleskySolveHelper(M, b, n, P) 164 | 165 | // check that the residual vector is 0. 166 | for(var i = 0; i < n; ++i) { 167 | var s = 0.0 168 | for(var k = 0; k < n; ++k) { 169 | s += A[i][k] * foundSolution[k] 170 | } 171 | var res = b[i] - s 172 | t.assert(almostEqual(0.0, res, eps, eps), "residual #" + i + ":" + "0.0 = " + res) 173 | } 174 | 175 | t.end(); 176 | }) 177 | --------------------------------------------------------------------------------