UNPKG

17.7 kBJavaScriptView Raw
1import { factory } from '../../../utils/factory'
2import { csFkeep } from './csFkeep'
3import { csFlip } from './csFlip'
4import { csTdfs } from './csTdfs'
5
6const name = 'csAmd'
7const dependencies = [
8 'add',
9 'multiply',
10 'transpose'
11]
12
13export const createCsAmd = /* #__PURE__ */ factory(name, dependencies, ({ add, multiply, transpose }) => {
14 /**
15 * Approximate minimum degree ordering. The minimum degree algorithm is a widely used
16 * heuristic for finding a permutation P so that P*A*P' has fewer nonzeros in its factorization
17 * than A. It is a gready method that selects the sparsest pivot row and column during the course
18 * of a right looking sparse Cholesky factorization.
19 *
20 * Reference: http://faculty.cse.tamu.edu/davis/publications.html
21 *
22 * @param {Number} order 0: Natural, 1: Cholesky, 2: LU, 3: QR
23 * @param {Matrix} m Sparse Matrix
24 */
25 return function csAmd (order, a) {
26 // check input parameters
27 if (!a || order <= 0 || order > 3) { return null }
28 // a matrix arrays
29 const asize = a._size
30 // rows and columns
31 const m = asize[0]
32 const n = asize[1]
33 // initialize vars
34 let lemax = 0
35 // dense threshold
36 let dense = Math.max(16, 10 * Math.sqrt(n))
37 dense = Math.min(n - 2, dense)
38 // create target matrix C
39 const cm = _createTargetMatrix(order, a, m, n, dense)
40 // drop diagonal entries
41 csFkeep(cm, _diag, null)
42 // C matrix arrays
43 const cindex = cm._index
44 const cptr = cm._ptr
45
46 // number of nonzero elements in C
47 let cnz = cptr[n]
48
49 // allocate result (n+1)
50 const P = []
51
52 // create workspace (8 * (n + 1))
53 const W = []
54 const len = 0 // first n + 1 entries
55 const nv = n + 1 // next n + 1 entries
56 const next = 2 * (n + 1) // next n + 1 entries
57 const head = 3 * (n + 1) // next n + 1 entries
58 const elen = 4 * (n + 1) // next n + 1 entries
59 const degree = 5 * (n + 1) // next n + 1 entries
60 const w = 6 * (n + 1) // next n + 1 entries
61 const hhead = 7 * (n + 1) // last n + 1 entries
62
63 // use P as workspace for last
64 const last = P
65
66 // initialize quotient graph
67 let mark = _initializeQuotientGraph(n, cptr, W, len, head, last, next, hhead, nv, w, elen, degree)
68
69 // initialize degree lists
70 let nel = _initializeDegreeLists(n, cptr, W, degree, elen, w, dense, nv, head, last, next)
71
72 // minimum degree node
73 let mindeg = 0
74
75 // vars
76 let i, j, k, k1, k2, e, pj, ln, nvi, pk, eln, p1, p2, pn, h, d
77
78 // while (selecting pivots) do
79 while (nel < n) {
80 // select node of minimum approximate degree. amd() is now ready to start eliminating the graph. It first
81 // finds a node k of minimum degree and removes it from its degree list. The variable nel keeps track of thow
82 // many nodes have been eliminated.
83 for (k = -1; mindeg < n && (k = W[head + mindeg]) === -1; mindeg++);
84 if (W[next + k] !== -1) { last[W[next + k]] = -1 }
85 // remove k from degree list
86 W[head + mindeg] = W[next + k]
87 // elenk = |Ek|
88 const elenk = W[elen + k]
89 // # of nodes k represents
90 let nvk = W[nv + k]
91 // W[nv + k] nodes of A eliminated
92 nel += nvk
93
94 // Construct a new element. The new element Lk is constructed in place if |Ek| = 0. nv[i] is
95 // negated for all nodes i in Lk to flag them as members of this set. Each node i is removed from the
96 // degree lists. All elements e in Ek are absorved into element k.
97 let dk = 0
98 // flag k as in Lk
99 W[nv + k] = -nvk
100 let p = cptr[k]
101 // do in place if W[elen + k] === 0
102 const pk1 = (elenk === 0) ? p : cnz
103 let pk2 = pk1
104 for (k1 = 1; k1 <= elenk + 1; k1++) {
105 if (k1 > elenk) {
106 // search the nodes in k
107 e = k
108 // list of nodes starts at cindex[pj]
109 pj = p
110 // length of list of nodes in k
111 ln = W[len + k] - elenk
112 } else {
113 // search the nodes in e
114 e = cindex[p++]
115 pj = cptr[e]
116 // length of list of nodes in e
117 ln = W[len + e]
118 }
119 for (k2 = 1; k2 <= ln; k2++) {
120 i = cindex[pj++]
121 // check node i dead, or seen
122 if ((nvi = W[nv + i]) <= 0) { continue }
123 // W[degree + Lk] += size of node i
124 dk += nvi
125 // negate W[nv + i] to denote i in Lk
126 W[nv + i] = -nvi
127 // place i in Lk
128 cindex[pk2++] = i
129 if (W[next + i] !== -1) { last[W[next + i]] = last[i] }
130 // check we need to remove i from degree list
131 if (last[i] !== -1) { W[next + last[i]] = W[next + i] } else { W[head + W[degree + i]] = W[next + i] }
132 }
133 if (e !== k) {
134 // absorb e into k
135 cptr[e] = csFlip(k)
136 // e is now a dead element
137 W[w + e] = 0
138 }
139 }
140 // cindex[cnz...nzmax] is free
141 if (elenk !== 0) { cnz = pk2 }
142 // external degree of k - |Lk\i|
143 W[degree + k] = dk
144 // element k is in cindex[pk1..pk2-1]
145 cptr[k] = pk1
146 W[len + k] = pk2 - pk1
147 // k is now an element
148 W[elen + k] = -2
149
150 // Find set differences. The scan1 function now computes the set differences |Le \ Lk| for all elements e. At the start of the
151 // scan, no entry in the w array is greater than or equal to mark.
152
153 // clear w if necessary
154 mark = _wclear(mark, lemax, W, w, n)
155 // scan 1: find |Le\Lk|
156 for (pk = pk1; pk < pk2; pk++) {
157 i = cindex[pk]
158 // check if W[elen + i] empty, skip it
159 if ((eln = W[elen + i]) <= 0) { continue }
160 // W[nv + i] was negated
161 nvi = -W[nv + i]
162 const wnvi = mark - nvi
163 // scan Ei
164 for (p = cptr[i], p1 = cptr[i] + eln - 1; p <= p1; p++) {
165 e = cindex[p]
166 if (W[w + e] >= mark) {
167 // decrement |Le\Lk|
168 W[w + e] -= nvi
169 } else if (W[w + e] !== 0) {
170 // ensure e is a live element, 1st time e seen in scan 1
171 W[w + e] = W[degree + e] + wnvi
172 }
173 }
174 }
175
176 // degree update
177 // The second pass computes the approximate degree di, prunes the sets Ei and Ai, and computes a hash
178 // function h(i) for all nodes in Lk.
179
180 // scan2: degree update
181 for (pk = pk1; pk < pk2; pk++) {
182 // consider node i in Lk
183 i = cindex[pk]
184 p1 = cptr[i]
185 p2 = p1 + W[elen + i] - 1
186 pn = p1
187 // scan Ei
188 for (h = 0, d = 0, p = p1; p <= p2; p++) {
189 e = cindex[p]
190 // check e is an unabsorbed element
191 if (W[w + e] !== 0) {
192 // dext = |Le\Lk|
193 const dext = W[w + e] - mark
194 if (dext > 0) {
195 // sum up the set differences
196 d += dext
197 // keep e in Ei
198 cindex[pn++] = e
199 // compute the hash of node i
200 h += e
201 } else {
202 // aggressive absorb. e->k
203 cptr[e] = csFlip(k)
204 // e is a dead element
205 W[w + e] = 0
206 }
207 }
208 }
209 // W[elen + i] = |Ei|
210 W[elen + i] = pn - p1 + 1
211 const p3 = pn
212 const p4 = p1 + W[len + i]
213 // prune edges in Ai
214 for (p = p2 + 1; p < p4; p++) {
215 j = cindex[p]
216 // check node j dead or in Lk
217 const nvj = W[nv + j]
218 if (nvj <= 0) { continue }
219 // degree(i) += |j|
220 d += nvj
221 // place j in node list of i
222 cindex[pn++] = j
223 // compute hash for node i
224 h += j
225 }
226 // check for mass elimination
227 if (d === 0) {
228 // absorb i into k
229 cptr[i] = csFlip(k)
230 nvi = -W[nv + i]
231 // |Lk| -= |i|
232 dk -= nvi
233 // |k| += W[nv + i]
234 nvk += nvi
235 nel += nvi
236 W[nv + i] = 0
237 // node i is dead
238 W[elen + i] = -1
239 } else {
240 // update degree(i)
241 W[degree + i] = Math.min(W[degree + i], d)
242 // move first node to end
243 cindex[pn] = cindex[p3]
244 // move 1st el. to end of Ei
245 cindex[p3] = cindex[p1]
246 // add k as 1st element in of Ei
247 cindex[p1] = k
248 // new len of adj. list of node i
249 W[len + i] = pn - p1 + 1
250 // finalize hash of i
251 h = (h < 0 ? -h : h) % n
252 // place i in hash bucket
253 W[next + i] = W[hhead + h]
254 W[hhead + h] = i
255 // save hash of i in last[i]
256 last[i] = h
257 }
258 }
259 // finalize |Lk|
260 W[degree + k] = dk
261 lemax = Math.max(lemax, dk)
262 // clear w
263 mark = _wclear(mark + lemax, lemax, W, w, n)
264
265 // Supernode detection. Supernode detection relies on the hash function h(i) computed for each node i.
266 // If two nodes have identical adjacency lists, their hash functions wil be identical.
267 for (pk = pk1; pk < pk2; pk++) {
268 i = cindex[pk]
269 // check i is dead, skip it
270 if (W[nv + i] >= 0) { continue }
271 // scan hash bucket of node i
272 h = last[i]
273 i = W[hhead + h]
274 // hash bucket will be empty
275 W[hhead + h] = -1
276 for (; i !== -1 && W[next + i] !== -1; i = W[next + i], mark++) {
277 ln = W[len + i]
278 eln = W[elen + i]
279 for (p = cptr[i] + 1; p <= cptr[i] + ln - 1; p++) { W[w + cindex[p]] = mark }
280 let jlast = i
281 // compare i with all j
282 for (j = W[next + i]; j !== -1;) {
283 let ok = W[len + j] === ln && W[elen + j] === eln
284 for (p = cptr[j] + 1; ok && p <= cptr[j] + ln - 1; p++) {
285 // compare i and j
286 if (W[w + cindex[p]] !== mark) { ok = 0 }
287 }
288 // check i and j are identical
289 if (ok) {
290 // absorb j into i
291 cptr[j] = csFlip(i)
292 W[nv + i] += W[nv + j]
293 W[nv + j] = 0
294 // node j is dead
295 W[elen + j] = -1
296 // delete j from hash bucket
297 j = W[next + j]
298 W[next + jlast] = j
299 } else {
300 // j and i are different
301 jlast = j
302 j = W[next + j]
303 }
304 }
305 }
306 }
307
308 // Finalize new element. The elimination of node k is nearly complete. All nodes i in Lk are scanned one last time.
309 // Node i is removed from Lk if it is dead. The flagged status of nv[i] is cleared.
310 for (p = pk1, pk = pk1; pk < pk2; pk++) {
311 i = cindex[pk]
312 // check i is dead, skip it
313 if ((nvi = -W[nv + i]) <= 0) { continue }
314 // restore W[nv + i]
315 W[nv + i] = nvi
316 // compute external degree(i)
317 d = W[degree + i] + dk - nvi
318 d = Math.min(d, n - nel - nvi)
319 if (W[head + d] !== -1) { last[W[head + d]] = i }
320 // put i back in degree list
321 W[next + i] = W[head + d]
322 last[i] = -1
323 W[head + d] = i
324 // find new minimum degree
325 mindeg = Math.min(mindeg, d)
326 W[degree + i] = d
327 // place i in Lk
328 cindex[p++] = i
329 }
330 // # nodes absorbed into k
331 W[nv + k] = nvk
332 // length of adj list of element k
333 if ((W[len + k] = p - pk1) === 0) {
334 // k is a root of the tree
335 cptr[k] = -1
336 // k is now a dead element
337 W[w + k] = 0
338 }
339 if (elenk !== 0) {
340 // free unused space in Lk
341 cnz = p
342 }
343 }
344
345 // Postordering. The elimination is complete, but no permutation has been computed. All that is left
346 // of the graph is the assembly tree (ptr) and a set of dead nodes and elements (i is a dead node if
347 // nv[i] is zero and a dead element if nv[i] > 0). It is from this information only that the final permutation
348 // is computed. The tree is restored by unflipping all of ptr.
349
350 // fix assembly tree
351 for (i = 0; i < n; i++) { cptr[i] = csFlip(cptr[i]) }
352 for (j = 0; j <= n; j++) { W[head + j] = -1 }
353 // place unordered nodes in lists
354 for (j = n; j >= 0; j--) {
355 // skip if j is an element
356 if (W[nv + j] > 0) { continue }
357 // place j in list of its parent
358 W[next + j] = W[head + cptr[j]]
359 W[head + cptr[j]] = j
360 }
361 // place elements in lists
362 for (e = n; e >= 0; e--) {
363 // skip unless e is an element
364 if (W[nv + e] <= 0) { continue }
365 if (cptr[e] !== -1) {
366 // place e in list of its parent
367 W[next + e] = W[head + cptr[e]]
368 W[head + cptr[e]] = e
369 }
370 }
371 // postorder the assembly tree
372 for (k = 0, i = 0; i <= n; i++) {
373 if (cptr[i] === -1) { k = csTdfs(i, k, W, head, next, P, w) }
374 }
375 // remove last item in array
376 P.splice(P.length - 1, 1)
377 // return P
378 return P
379 }
380
381 /**
382 * Creates the matrix that will be used by the approximate minimum degree ordering algorithm. The function accepts the matrix M as input and returns a permutation
383 * vector P. The amd algorithm operates on a symmetrix matrix, so one of three symmetric matrices is formed.
384 *
385 * Order: 0
386 * A natural ordering P=null matrix is returned.
387 *
388 * Order: 1
389 * Matrix must be square. This is appropriate for a Cholesky or LU factorization.
390 * P = M + M'
391 *
392 * Order: 2
393 * Dense columns from M' are dropped, M recreated from M'. This is appropriatefor LU factorization of unsymmetric matrices.
394 * P = M' * M
395 *
396 * Order: 3
397 * This is best used for QR factorization or LU factorization is matrix M has no dense rows. A dense row is a row with more than 10*sqr(columns) entries.
398 * P = M' * M
399 */
400 function _createTargetMatrix (order, a, m, n, dense) {
401 // compute A'
402 const at = transpose(a)
403
404 // check order = 1, matrix must be square
405 if (order === 1 && n === m) {
406 // C = A + A'
407 return add(a, at)
408 }
409
410 // check order = 2, drop dense columns from M'
411 if (order === 2) {
412 // transpose arrays
413 const tindex = at._index
414 const tptr = at._ptr
415 // new column index
416 let p2 = 0
417 // loop A' columns (rows)
418 for (let j = 0; j < m; j++) {
419 // column j of AT starts here
420 let p = tptr[j]
421 // new column j starts here
422 tptr[j] = p2
423 // skip dense col j
424 if (tptr[j + 1] - p > dense) { continue }
425 // map rows in column j of A
426 for (const p1 = tptr[j + 1]; p < p1; p++) { tindex[p2++] = tindex[p] }
427 }
428 // finalize AT
429 tptr[m] = p2
430 // recreate A from new transpose matrix
431 a = transpose(at)
432 // use A' * A
433 return multiply(at, a)
434 }
435
436 // use A' * A, square or rectangular matrix
437 return multiply(at, a)
438 }
439
440 /**
441 * Initialize quotient graph. There are four kind of nodes and elements that must be represented:
442 *
443 * - A live node is a node i (or a supernode) that has not been selected as a pivot nad has not been merged into another supernode.
444 * - A dead node i is one that has been removed from the graph, having been absorved into r = flip(ptr[i]).
445 * - A live element e is one that is in the graph, having been formed when node e was selected as the pivot.
446 * - A dead element e is one that has benn absorved into a subsequent element s = flip(ptr[e]).
447 */
448 function _initializeQuotientGraph (n, cptr, W, len, head, last, next, hhead, nv, w, elen, degree) {
449 // Initialize quotient graph
450 for (let k = 0; k < n; k++) { W[len + k] = cptr[k + 1] - cptr[k] }
451 W[len + n] = 0
452 // initialize workspace
453 for (let i = 0; i <= n; i++) {
454 // degree list i is empty
455 W[head + i] = -1
456 last[i] = -1
457 W[next + i] = -1
458 // hash list i is empty
459 W[hhead + i] = -1
460 // node i is just one node
461 W[nv + i] = 1
462 // node i is alive
463 W[w + i] = 1
464 // Ek of node i is empty
465 W[elen + i] = 0
466 // degree of node i
467 W[degree + i] = W[len + i]
468 }
469 // clear w
470 const mark = _wclear(0, 0, W, w, n)
471 // n is a dead element
472 W[elen + n] = -2
473 // n is a root of assembly tree
474 cptr[n] = -1
475 // n is a dead element
476 W[w + n] = 0
477 // return mark
478 return mark
479 }
480
481 /**
482 * Initialize degree lists. Each node is placed in its degree lists. Nodes of zero degree are eliminated immediately. Nodes with
483 * degree >= dense are alsol eliminated and merged into a placeholder node n, a dead element. Thes nodes will appera last in the
484 * output permutation p.
485 */
486 function _initializeDegreeLists (n, cptr, W, degree, elen, w, dense, nv, head, last, next) {
487 // result
488 let nel = 0
489 // loop columns
490 for (let i = 0; i < n; i++) {
491 // degree @ i
492 const d = W[degree + i]
493 // check node i is empty
494 if (d === 0) {
495 // element i is dead
496 W[elen + i] = -2
497 nel++
498 // i is a root of assembly tree
499 cptr[i] = -1
500 W[w + i] = 0
501 } else if (d > dense) {
502 // absorb i into element n
503 W[nv + i] = 0
504 // node i is dead
505 W[elen + i] = -1
506 nel++
507 cptr[i] = csFlip(n)
508 W[nv + n]++
509 } else {
510 const h = W[head + d]
511 if (h !== -1) { last[h] = i }
512 // put node i in degree list d
513 W[next + i] = W[head + d]
514 W[head + d] = i
515 }
516 }
517 return nel
518 }
519
520 function _wclear (mark, lemax, W, w, n) {
521 if (mark < 2 || (mark + lemax < 0)) {
522 for (let k = 0; k < n; k++) {
523 if (W[w + k] !== 0) { W[w + k] = 1 }
524 }
525 mark = 2
526 }
527 // at this point, W [0..n-1] < mark holds
528 return mark
529 }
530
531 function _diag (i, j) {
532 return i !== j
533 }
534})