1 | import { factory } from '../../../utils/factory'
|
2 | import { csFkeep } from './csFkeep'
|
3 | import { csFlip } from './csFlip'
|
4 | import { csTdfs } from './csTdfs'
|
5 |
|
6 | const name = 'csAmd'
|
7 | const dependencies = [
|
8 | 'add',
|
9 | 'multiply',
|
10 | 'transpose'
|
11 | ]
|
12 |
|
13 | export 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 | })
|