@graphty/cytoscape-extensions 0.0.28 → 0.0.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/algorithm-options.d.ts +5 -0
  2. package/dist/algorithm-options.d.ts.map +1 -0
  3. package/dist/algorithm-options.js +285 -0
  4. package/dist/algorithm-options.js.map +1 -0
  5. package/dist/algorithms.d.ts +489 -0
  6. package/dist/algorithms.d.ts.map +1 -0
  7. package/dist/algorithms.js +1184 -0
  8. package/dist/algorithms.js.map +1 -0
  9. package/dist/cdn/chunks/elements-DIunzJUl.js +1 -0
  10. package/dist/cdn/chunks/gpu-platform-browser-DnQwf3s0.js +2825 -0
  11. package/dist/cdn/chunks/index-C-jUF_Ve.js +1 -0
  12. package/dist/cdn/chunks/index-C4RZkds_.js +10 -0
  13. package/dist/cdn/chunks/io-D-Vr0j3W.js +269 -0
  14. package/dist/cdn/chunks/preload-helper-Crt_PPe8.js +1 -0
  15. package/dist/cdn/chunks/samples-Dja23zzq.js +1 -0
  16. package/dist/cdn/chunks/sigv4-BXwVbza3-DZCCNyQA.js +3 -0
  17. package/dist/cdn/chunks/sources-cypher-oj3S6pKb.js +5 -0
  18. package/dist/cdn/chunks/sources-ndex-BgZhxHsL.js +1 -0
  19. package/dist/cdn/chunks/sources-rest-E6On5P3z.js +1 -0
  20. package/dist/cdn/chunks/sources-sparql-BAxh1dw-.js +5 -0
  21. package/dist/cdn/cytoscape-extensions.js +1 -0
  22. package/dist/cdn/io.js +1 -0
  23. package/dist/cytoscape-extensions.bundle.js +2825 -0
  24. package/dist/elements.d.ts +69 -0
  25. package/dist/elements.d.ts.map +1 -0
  26. package/dist/elements.js +343 -0
  27. package/dist/elements.js.map +1 -0
  28. package/dist/gpu-platform-browser.d.ts +11 -0
  29. package/dist/gpu-platform-browser.d.ts.map +1 -0
  30. package/dist/gpu-platform-browser.js +22 -0
  31. package/dist/gpu-platform-browser.js.map +1 -0
  32. package/dist/gpu-platform-node.d.ts +15 -0
  33. package/dist/gpu-platform-node.d.ts.map +1 -0
  34. package/dist/gpu-platform-node.js +27 -0
  35. package/dist/gpu-platform-node.js.map +1 -0
  36. package/dist/gpu.d.ts +149 -0
  37. package/dist/gpu.d.ts.map +1 -0
  38. package/dist/gpu.js +312 -0
  39. package/dist/gpu.js.map +1 -0
  40. package/dist/graph-data.d.ts +101 -0
  41. package/dist/graph-data.d.ts.map +1 -0
  42. package/dist/graph-data.js +147 -0
  43. package/dist/graph-data.js.map +1 -0
  44. package/dist/index.d.ts +36 -0
  45. package/dist/index.d.ts.map +1 -0
  46. package/dist/index.js +48 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/io.d.ts +111 -0
  49. package/dist/io.d.ts.map +1 -0
  50. package/dist/io.js +258 -0
  51. package/dist/io.js.map +1 -0
  52. package/dist/layouts.d.ts +207 -0
  53. package/dist/layouts.d.ts.map +1 -0
  54. package/dist/layouts.js +615 -0
  55. package/dist/layouts.js.map +1 -0
  56. package/dist/lazy.d.ts +21 -0
  57. package/dist/lazy.d.ts.map +1 -0
  58. package/dist/lazy.js +32 -0
  59. package/dist/lazy.js.map +1 -0
  60. package/dist/samples.d.ts +115 -0
  61. package/dist/samples.d.ts.map +1 -0
  62. package/dist/samples.js +237 -0
  63. package/dist/samples.js.map +1 -0
  64. package/dist/snapshot.d.ts +81 -0
  65. package/dist/snapshot.d.ts.map +1 -0
  66. package/dist/snapshot.js +225 -0
  67. package/dist/snapshot.js.map +1 -0
  68. package/dist/webgpu-provider.d.ts +25 -0
  69. package/dist/webgpu-provider.d.ts.map +1 -0
  70. package/dist/webgpu-provider.js +53 -0
  71. package/dist/webgpu-provider.js.map +1 -0
  72. package/package.json +13 -9
@@ -0,0 +1,1184 @@
1
+ /**
2
+ * Every @graphty/algorithms algorithm as a Cytoscape.js 3.x collection and core method, named "graphty<Name>".
3
+ *
4
+ * The conventions, which follow Cytoscape's built-in algorithms:
5
+ * - scope is the calling collection (its nodes, and its edges whose two ends are in it); `cy.graphtyX(o)` is
6
+ * `cy.elements().graphtyX(o)`;
7
+ * - synchronous, one options object;
8
+ * - `directed` (default false) and `weight` (an edge data field, or a function of the edge) describe the graph;
9
+ * - nodes are named by a selector or a collection (`root`, `goal`, `source`, `sink`, ...);
10
+ * - results are keyed by element: accessor functions (`score(node)`, `distanceTo(node)`), collections, and arrays
11
+ * of collections, never index arrays;
12
+ * - `field` writes the per-element value (what `score` or `cluster` returns) into `data(field)`, so a stylesheet
13
+ * can map it;
14
+ * - every other option the method's type lists goes to the @graphty/algorithms function unchanged, and an option it
15
+ * does not list throws (src/algorithm-options.ts, generated from those types);
16
+ * - errors throw, from the adapter (a selection that matches nothing, a weight given to an algorithm that ignores
17
+ * weights, an unknown option) and from the algorithm (an undirected graph given to one that needs a directed one),
18
+ * each message starting with the method name.
19
+ */
20
+ import { accelerated, adamicAdarForPairs, adamicAdarPrediction, adamicAdarScore, allPairsShortestPath, astar, bellmanFord, betweennessCentrality, bidirectionalDijkstra, breadthFirstSearch, closenessCentrality, commonNeighborsForPairs, commonNeighborsPrediction, commonNeighborsScore, condensation, connectedComponents, degreeCentrality, degrees, DeltaPageRank, depthFirstSearch, dijkstra, directionOptimizedBfs, edgeBetweennessCentrality, eigenvectorCentrality, evaluateAdamicAdar, evaluateCommonNeighbors, findAllIsomorphisms, getTopAdamicAdarCandidatesForNode, getTopCandidatesForNode, girvanNewman, greedyBipartiteMatching, grsbm, hasCycle, hierarchicalClustering, hits, isBipartite, isGraphIsomorphic, kargerMinCut, katzCentrality, kCoreDecomposition, kruskalMST, labelPropagation, labelPropagationSemiSupervised, labelPropagationSynchronous, leiden, louvain, markovClustering, maxFlow, maximumBipartiteMatching, minSTCut, modularity, nodeClosenessCentrality, pageRank, personalizedPageRank, primMST, PriorityDeltaPageRank, spectralClustering, stoerWagner, stronglyConnectedComponents, syncClustering, teraHAC, topologicalSort, triangleCount, weaklyConnectedComponents, } from "@graphty/algorithms";
21
+ import { INVALID_INDEX, makeMask, maskSet, maskTest, } from "@graphty/graph-format";
22
+ import { OPTION_NAMES } from "./algorithm-options.js";
23
+ import { backendOf, checkGpuMode, coreDestroyedError, gpuFor, recording, warnIfFixable, } from "./gpu.js";
24
+ import { coreOf, hold, indexOf, indicesOf, toSnapshot, writeData, } from "./snapshot.js";
25
+ /**
26
+ * The CPU functions behind the dispatcher methods the WebGPU accelerator implements (its `createAccelerator`
27
+ * list), under the dispatcher's names. The plain methods call these directly, so they stay synchronous.
28
+ */
29
+ const CPU = {
30
+ breadthFirstSearch,
31
+ sssp: dijkstra,
32
+ bellmanFord,
33
+ allPairsShortestPath,
34
+ pageRank,
35
+ personalizedPageRank,
36
+ eigenvectorCentrality,
37
+ katzCentrality,
38
+ hits,
39
+ closenessCentrality,
40
+ betweennessCentrality,
41
+ edgeBetweennessCentrality,
42
+ connectedComponents,
43
+ weaklyConnectedComponents,
44
+ triangleCount,
45
+ labelPropagation,
46
+ labelPropagationSynchronous,
47
+ };
48
+ /**
49
+ * Applies a function to a value that is either plain (a CPU run) or a promise (a dispatcher run).
50
+ * @param x - the value or its promise
51
+ * @param f - the function
52
+ * @returns f(x), or its promise
53
+ */
54
+ function then(x, f) {
55
+ return x instanceof Promise ? x.then(f) : f(x);
56
+ }
57
+ // Options the adapter consumes and converts; everything else passes through to the algorithm.
58
+ const ADAPTER_KEYS = new Set([
59
+ "gpu",
60
+ "directed",
61
+ "weight",
62
+ "field",
63
+ "root",
64
+ "goal",
65
+ "target",
66
+ "source",
67
+ "sink",
68
+ "heuristic",
69
+ "personalization",
70
+ "seeds",
71
+ "left",
72
+ "right",
73
+ "clusters",
74
+ "pairs",
75
+ "edges",
76
+ "nonEdges",
77
+ "nodeMatch",
78
+ "edgeMatch",
79
+ "priority",
80
+ "candidates",
81
+ "sources",
82
+ "alive",
83
+ "startVector",
84
+ "initialRanks",
85
+ ]);
86
+ // Algorithms that read no weights: a `weight` option would be silently ignored, so it throws.
87
+ export const UNWEIGHTED = new Set([
88
+ "breadthFirstSearch",
89
+ "directionOptimizedBfs",
90
+ "depthFirstSearch",
91
+ "hasCycle",
92
+ "topologicalSort",
93
+ "degreeCentrality",
94
+ "degrees",
95
+ "eigenvectorCentrality",
96
+ "betweennessCentrality",
97
+ "edgeBetweennessCentrality",
98
+ "connectedComponents",
99
+ "weaklyConnectedComponents",
100
+ "stronglyConnectedComponents",
101
+ "condensation",
102
+ "kCoreDecomposition",
103
+ "triangleCount",
104
+ "isBipartite",
105
+ "isGraphIsomorphic",
106
+ "findAllIsomorphisms",
107
+ "hierarchicalClustering",
108
+ "teraHAC",
109
+ "syncClustering",
110
+ "maximumBipartiteMatching",
111
+ "greedyBipartiteMatching",
112
+ "commonNeighborsScore",
113
+ "adamicAdarScore",
114
+ "commonNeighborsPrediction",
115
+ "adamicAdarPrediction",
116
+ "commonNeighborsForPairs",
117
+ "adamicAdarForPairs",
118
+ "topCandidatesForNode",
119
+ "topAdamicAdarCandidatesForNode",
120
+ "evaluateCommonNeighbors",
121
+ "evaluateAdamicAdar",
122
+ "compareAdamicAdarWithCommonNeighbors",
123
+ ]);
124
+ // Algorithms whose result has no per-element value to write: a `field` option would be silently ignored, so it throws.
125
+ export const NO_FIELD = new Set([
126
+ "hasCycle",
127
+ "topologicalSort",
128
+ "bidirectionalDijkstra",
129
+ "aStar",
130
+ "allPairsShortestPath",
131
+ "degrees",
132
+ "nodeClosenessCentrality",
133
+ "isGraphIsomorphic",
134
+ "findAllIsomorphisms",
135
+ "modularity",
136
+ "hierarchicalClustering",
137
+ "kruskalMST",
138
+ "primMST",
139
+ "maximumBipartiteMatching",
140
+ "greedyBipartiteMatching",
141
+ "commonNeighborsScore",
142
+ "adamicAdarScore",
143
+ "commonNeighborsPrediction",
144
+ "adamicAdarPrediction",
145
+ "commonNeighborsForPairs",
146
+ "adamicAdarForPairs",
147
+ "topCandidatesForNode",
148
+ "topAdamicAdarCandidatesForNode",
149
+ "evaluateCommonNeighbors",
150
+ "evaluateAdamicAdar",
151
+ "compareAdamicAdarWithCommonNeighbors",
152
+ ]);
153
+ // This package's own defaults, passed explicitly to @graphty/algorithms (and through it to the GPU), so a change of
154
+ // a library default does not change what a Cytoscape user gets. docs/reference/algorithms.md shows them; a caller's
155
+ // option overrides them. `weighted` is pinned separately: it is true exactly when the caller passed `weight`.
156
+ const POWER = { maxIterations: 100, tolerance: 1e-6 };
157
+ const PAGERANK = { ...POWER, dampingFactor: 0.85 };
158
+ export const DEFAULTS = {
159
+ pageRank: PAGERANK,
160
+ personalizedPageRank: PAGERANK,
161
+ deltaPageRank: PAGERANK,
162
+ eigenvectorCentrality: POWER,
163
+ hits: POWER,
164
+ katzCentrality: { ...POWER, alpha: 0.1, beta: 1 },
165
+ labelPropagation: { maxIterations: 100 },
166
+ labelPropagationSynchronous: { maxIterations: 100 },
167
+ labelPropagationSemiSupervised: { maxIterations: 100 },
168
+ allPairsShortestPath: { paths: true },
169
+ maxFlow: { algorithm: "edmonds-karp" },
170
+ minSTCut: { algorithm: "ford-fulkerson" },
171
+ };
172
+ // Algorithms defined only for directed graphs: `directed` defaults to true.
173
+ export const DIRECTED = new Set([
174
+ "topologicalSort",
175
+ "stronglyConnectedComponents",
176
+ "condensation",
177
+ "deltaPageRank",
178
+ ]);
179
+ // ---------------------------------------------------------------------------------------------------------------
180
+ // Conversions between elements and indices
181
+ /**
182
+ * A collection of elements, in the given order. Cytoscape accepts an element array at run time; its typings declare
183
+ * only element definitions, hence the cast.
184
+ * @param c - the context
185
+ * @param eles - the elements
186
+ * @returns the collection
187
+ */
188
+ function collect(c, eles) {
189
+ return c.cy.collection(eles);
190
+ }
191
+ /**
192
+ * Throws on a negative edge weight, which Dijkstra and A* cannot take (on an undirected graph Dijkstra would never
193
+ * return). The code matches graphtyBidirectionalDijkstra's error.
194
+ * @param c - the context
195
+ */
196
+ function nonNegative(c) {
197
+ if (c.s.weights?.some((w) => w < 0) === true) {
198
+ throw Object.assign(new Error(`${c.name}: a weight is negative; use graphtyBellmanFord`), {
199
+ code: "E_BAD_WEIGHT",
200
+ });
201
+ }
202
+ }
203
+ /**
204
+ * The index of a required node option.
205
+ * @param c - the context
206
+ * @param sel - the option value
207
+ * @param what - the option name
208
+ * @returns the node index
209
+ */
210
+ function req(c, sel, what) {
211
+ const i = indexOf(c.cs, sel, `${c.name}: ${what}`);
212
+ if (i === undefined) {
213
+ throw new Error(`${c.name}: the ${what} option is required`);
214
+ }
215
+ return i;
216
+ }
217
+ /**
218
+ * The index of a node or edge reference, or undefined when it is not in the snapshot.
219
+ * @param c - the context
220
+ * @param ref - an element or a selector
221
+ * @param edges - look among the edges instead of the nodes
222
+ * @returns the index
223
+ */
224
+ function lookup(c, ref, edges = false) {
225
+ const list = (edges ? c.cs.edges : c.cs.nodes);
226
+ // a selector goes through Cytoscape's own matching (a collection has no find() that takes one)
227
+ const matches = typeof ref === "string" ? list.filter(ref) : ref;
228
+ const ele = matches[0];
229
+ if (ele === undefined) {
230
+ return undefined;
231
+ }
232
+ if (!edges) {
233
+ const i = c.s.ids.indexOf(ele.id());
234
+ return i === INVALID_INDEX ? undefined : i;
235
+ }
236
+ // ponytail: linear scan per edge lookup; an id -> index map if edge accessors show up in a profile.
237
+ const i = c.cs.edges.toArray().indexOf(ele);
238
+ return i < 0 ? undefined : i;
239
+ }
240
+ /**
241
+ * An accessor from element to value.
242
+ * @param c - the context
243
+ * @param values - one value per node (or edge) index
244
+ * @param edges - the values are per edge
245
+ * @returns the accessor
246
+ */
247
+ function accessor(c, values, edges = false) {
248
+ return (ref) => {
249
+ const i = lookup(c, ref, edges);
250
+ return i === undefined ? undefined : values[i];
251
+ };
252
+ }
253
+ /**
254
+ * The nodes at some indices, as a collection in that order.
255
+ * @param c - the context
256
+ * @param idx - node indices
257
+ * @returns the collection
258
+ */
259
+ function nodesAt(c, idx) {
260
+ return collect(c, Array.from(idx, (i) => c.cs.nodes[i]));
261
+ }
262
+ /**
263
+ * The edges at some indices, as a collection in that order.
264
+ * @param c - the context
265
+ * @param idx - edge indices
266
+ * @returns the collection
267
+ */
268
+ function edgesAt(c, idx) {
269
+ return collect(c, Array.from(idx, (i) => c.cs.edges[i]));
270
+ }
271
+ /**
272
+ * A path as Cytoscape's path algorithms return it: node, edge, node, ...
273
+ * @param c - the context
274
+ * @param nodes - node indices along the path
275
+ * @param edges - edge indices along the path, one fewer
276
+ * @returns the collection
277
+ */
278
+ function pathOf(c, nodes, edges) {
279
+ const out = [];
280
+ for (let k = 0; k < nodes.length; k++) {
281
+ if (k > 0) {
282
+ out.push(c.cs.edges[edges[k - 1]]);
283
+ }
284
+ out.push(c.cs.nodes[nodes[k]]);
285
+ }
286
+ return collect(c, out);
287
+ }
288
+ /**
289
+ * The node indices of each selection in a list.
290
+ * @param c - the context
291
+ * @param pairs - node pairs
292
+ * @param what - the option name
293
+ * @returns the sources and targets
294
+ */
295
+ function pairsOf(c, pairs, what) {
296
+ if (pairs === undefined) {
297
+ throw new Error(`${c.name}: the ${what} option is required`);
298
+ }
299
+ return { sources: pairs.map((p) => req(c, p[0], what)), targets: pairs.map((p) => req(c, p[1], what)) };
300
+ }
301
+ /**
302
+ * A node mask of a selection.
303
+ * @param c - the context
304
+ * @param sel - a selection, or undefined
305
+ * @returns the mask, or undefined
306
+ */
307
+ function maskOf(c, sel) {
308
+ if (sel === undefined) {
309
+ return undefined;
310
+ }
311
+ const m = makeMask(c.s.nodeCount);
312
+ for (const i of indicesOf(c.cs, sel)) {
313
+ maskSet(m, i, true);
314
+ }
315
+ return m;
316
+ }
317
+ // ---------------------------------------------------------------------------------------------------------------
318
+ // Result builders
319
+ /**
320
+ * A score result: `score(ele)` plus extras; writes `field`.
321
+ * @param c - the context
322
+ * @param values - one value per node (or edge) index
323
+ * @param extra - other fields of the result
324
+ * @param edges - the values are per edge
325
+ * @returns the result
326
+ */
327
+ function scored(c, values, extra, edges = false) {
328
+ if (c.field !== undefined) {
329
+ writeData(edges ? c.cs.edges : c.cs.nodes, values, c.field);
330
+ }
331
+ return { score: accessor(c, values, edges), ...extra };
332
+ }
333
+ /**
334
+ * A partition: one node collection per label, with `cluster(node)` and extras; writes `field`.
335
+ * @param c - the context
336
+ * @param labels - the label of every node index, 0..count-1 (INVALID_INDEX: in no cluster)
337
+ * @param extra - other fields of the result
338
+ * @returns the partition
339
+ */
340
+ function partition(c, labels, extra) {
341
+ const groups = [];
342
+ for (let i = 0; i < labels.length; i++) {
343
+ const l = labels[i];
344
+ if (l !== INVALID_INDEX) {
345
+ const group = groups[l] ?? [];
346
+ group.push(i);
347
+ groups[l] = group;
348
+ }
349
+ }
350
+ if (c.field !== undefined) {
351
+ writeData(c.cs.nodes, labels, c.field);
352
+ }
353
+ const out = Array.from(groups, (g) => nodesAt(c, g ?? []));
354
+ return Object.assign(out, extra, {
355
+ cluster: (ref) => {
356
+ const i = lookup(c, ref);
357
+ return i === undefined || labels[i] === INVALID_INDEX ? undefined : labels[i];
358
+ },
359
+ });
360
+ }
361
+ /**
362
+ * A walk result (BFS, DFS).
363
+ * @param c - the context
364
+ * @param r - the walk
365
+ * @param r.order - visited node indices in visit order
366
+ * @param r.parent - parent node index per node
367
+ * @param r.depth - depth per node
368
+ * @param r.visitedCount - number of visited nodes
369
+ * @param target - the target index, if any
370
+ * @returns the search result
371
+ */
372
+ function search(c, r, target) {
373
+ const out = [];
374
+ for (let k = 0; k < r.visitedCount; k++) {
375
+ const v = r.order[k];
376
+ const p = r.parent[v];
377
+ if (p !== INVALID_INDEX) {
378
+ // The walk records the parent node, not the arc, so the tree edge is looked up: the first edge of the
379
+ // collection joining the two (CPU BFS/DFS results have no `predArc`, unlike Dijkstra's).
380
+ const from = c.cs.nodes[p];
381
+ const to = c.cs.nodes[v];
382
+ const joining = c.s.directed ? from.edgesTo(to) : from.edgesWith(to);
383
+ const e = joining.intersection(c.cs.edges)[0];
384
+ if (e !== undefined) {
385
+ out.push(e);
386
+ }
387
+ }
388
+ out.push(c.cs.nodes[v]);
389
+ }
390
+ const reached = target !== undefined && r.depth[target] !== INVALID_INDEX;
391
+ if (c.field !== undefined) {
392
+ writeData(c.cs.nodes, Array.from(r.depth, (d) => (d === INVALID_INDEX ? Number.NaN : d)), c.field);
393
+ }
394
+ return {
395
+ path: collect(c, out),
396
+ found: collect(c, reached ? [c.cs.nodes[target]] : []),
397
+ depth: (ref) => {
398
+ const i = lookup(c, ref);
399
+ return i === undefined || r.depth[i] === INVALID_INDEX ? undefined : r.depth[i];
400
+ },
401
+ parent: (ref) => {
402
+ const i = lookup(c, ref);
403
+ return i === undefined || r.parent[i] === INVALID_INDEX ? undefined : c.cs.nodes[r.parent[i]];
404
+ },
405
+ };
406
+ }
407
+ /**
408
+ * A single-source shortest-path result.
409
+ * @param c - the context
410
+ * @param r - the result
411
+ * @param r.dist - distance per node
412
+ * @param r.pathTo - node path to a target
413
+ * @param r.pathEdges - edge path to a target
414
+ * @returns the accessors
415
+ */
416
+ function paths(c, r) {
417
+ if (c.field !== undefined) {
418
+ writeData(c.cs.nodes, r.dist, c.field);
419
+ }
420
+ return {
421
+ distanceTo: (ref) => {
422
+ const i = lookup(c, ref);
423
+ return i === undefined ? Infinity : r.dist[i];
424
+ },
425
+ pathTo: (ref) => {
426
+ const i = lookup(c, ref);
427
+ return i === undefined ? c.cy.collection() : pathOf(c, r.pathTo(i), r.pathEdges(i));
428
+ },
429
+ };
430
+ }
431
+ /**
432
+ * A cut result.
433
+ * @param c - the context
434
+ * @param side - the first side as a node mask
435
+ * @param cutEdges - the cut edge indices
436
+ * @param value - the cut's weight
437
+ * @returns the cut
438
+ */
439
+ function cutOf(c, side, cutEdges, value) {
440
+ const first = [];
441
+ const second = [];
442
+ for (let i = 0; i < c.s.nodeCount; i++) {
443
+ (maskTest(side, i) ? first : second).push(i);
444
+ }
445
+ if (c.field !== undefined) {
446
+ writeData(c.cs.nodes, Array.from({ length: c.s.nodeCount }, (_, i) => (maskTest(side, i) ? 0 : 1)), c.field);
447
+ }
448
+ return { value, cut: edgesAt(c, cutEdges), partitionFirst: nodesAt(c, first), partitionSecond: nodesAt(c, second) };
449
+ }
450
+ /**
451
+ * A minimum cut result.
452
+ * @param c - the context
453
+ * @param r - the cut
454
+ * @returns the cut
455
+ */
456
+ function minCut(c, r) {
457
+ return cutOf(c, r.side, r.cutEdges, r.cutValue);
458
+ }
459
+ /**
460
+ * Predicted links with their nodes.
461
+ * @param c - the context
462
+ * @param r - the prediction
463
+ * @returns one entry per predicted pair, best first
464
+ */
465
+ function links(c, r) {
466
+ return Array.from(r.scores, (score, k) => ({
467
+ source: c.cs.nodes[r.sources[k]],
468
+ target: c.cs.nodes[r.targets[k]],
469
+ score,
470
+ }));
471
+ }
472
+ /**
473
+ * A node-to-node mapping accessor.
474
+ * @param c - the context
475
+ * @param other - the image collection's snapshot
476
+ * @param mapping - image index per node index
477
+ * @returns the accessor
478
+ */
479
+ function mapper(c, other, mapping) {
480
+ return (ref) => {
481
+ const i = lookup(c, ref);
482
+ return i === undefined ? undefined : other.nodes[mapping[i]];
483
+ };
484
+ }
485
+ /**
486
+ * The two-snapshot isomorphism options, with element callbacks turned into index callbacks.
487
+ * @param c - the context
488
+ * @param other - the second graph
489
+ * @param o - the options
490
+ * @returns the options for @graphty/algorithms
491
+ */
492
+ function isoOptions(c, other, o) {
493
+ const { nodeMatch, edgeMatch } = o;
494
+ return {
495
+ ...c.rest,
496
+ nodeMatch: nodeMatch && ((a, b) => nodeMatch(c.cs.nodes[a], other.nodes[b])),
497
+ edgeMatch: edgeMatch && ((a, b) => edgeMatch(c.cs.edges[a], other.edges[b])),
498
+ };
499
+ }
500
+ /**
501
+ * The snapshot of the second graph of an isomorphism test, read like the first.
502
+ * @param c - the context
503
+ * @param o - the options naming it
504
+ * @returns its snapshot
505
+ */
506
+ function otherOf(c, o) {
507
+ const other = o.other;
508
+ // required by the type, checked for callers without types
509
+ if (other === undefined) {
510
+ throw new Error(`${c.name}: the other option (the collection to compare with) is required`);
511
+ }
512
+ // a selector (over the whole core) or an array of elements names the other graph too
513
+ let eles = other;
514
+ if (typeof other === "string") {
515
+ eles = c.cy.$(other);
516
+ }
517
+ else if (Array.isArray(other)) {
518
+ // Cytoscape takes an array of elements here; its typings list only definitions
519
+ eles = c.cy.collection(other);
520
+ }
521
+ if (typeof eles?.nodes !== "function") {
522
+ throw new TypeError(`${c.name}: the other option must be a Cytoscape collection, a selector or an array of elements`);
523
+ }
524
+ return toSnapshot(eles, { directed: c.s.directed });
525
+ }
526
+ /**
527
+ * A per-node vector from a function of the node or a selection (1 for selected nodes, 0 for the rest).
528
+ * @param c - the context
529
+ * @param v - the function or selection
530
+ * @param what - the option name
531
+ * @returns the vector
532
+ */
533
+ function vectorOf(c, v, what) {
534
+ if (v === undefined) {
535
+ throw new Error(`${c.name}: the ${what} option is required`);
536
+ }
537
+ if (typeof v === "function") {
538
+ return Float64Array.from(c.cs.nodes.toArray(), (n) => v(n));
539
+ }
540
+ const out = new Float64Array(c.s.nodeCount);
541
+ for (const i of indicesOf(c.cs, v)) {
542
+ out[i] = 1;
543
+ }
544
+ return out;
545
+ }
546
+ /**
547
+ * The teleport vector of a personalized PageRank. One that selects no node would run plain PageRank without a word,
548
+ * so it throws, as a required node option that matches nothing does.
549
+ * @param c - the context
550
+ * @param v - the selection or the weight function
551
+ * @returns the vector
552
+ */
553
+ function teleportOf(c, v) {
554
+ const p = vectorOf(c, v, "personalization");
555
+ if (!p.some((x) => x > 0)) {
556
+ throw new Error(`${c.name}: personalization ${typeof v === "function" ? "gives no node a positive weight" : "matches no node"}`);
557
+ }
558
+ return p;
559
+ }
560
+ /**
561
+ * Labels per node from a list of clusters or a node data field.
562
+ * @param c - the context
563
+ * @param clusters - the clusters, or the data field holding each node's cluster
564
+ * @param what - the option name, for the error
565
+ * @returns label per node index (INVALID_INDEX: none)
566
+ */
567
+ function labelsOf(c, clusters, what) {
568
+ if (clusters === undefined) {
569
+ throw new Error(`${c.name}: the ${what} option is required`);
570
+ }
571
+ const labels = new Uint32Array(c.s.nodeCount).fill(INVALID_INDEX);
572
+ if (typeof clusters === "string") {
573
+ const ids = new Map();
574
+ c.cs.nodes.forEach((n, i) => {
575
+ const v = n.data(clusters);
576
+ if (v !== undefined) {
577
+ labels[i] = ids.get(v) ?? ids.set(v, ids.size).size - 1;
578
+ }
579
+ });
580
+ }
581
+ else {
582
+ clusters.forEach((sel, k) => {
583
+ for (const i of indicesOf(c.cs, sel)) {
584
+ labels[i] = k;
585
+ }
586
+ });
587
+ }
588
+ // a misspelled field or selector would otherwise give a plausible result (modularity 0, an unseeded partition)
589
+ if (labels.every((l) => l === INVALID_INDEX)) {
590
+ throw new Error(typeof clusters === "string"
591
+ ? `${c.name}: ${what} names the data field "${clusters}", which no node has`
592
+ : `${c.name}: ${what} matches no node`);
593
+ }
594
+ return labels;
595
+ }
596
+ /**
597
+ * The all-pairs accessors.
598
+ * @param c - the context
599
+ * @param r - the distance matrix, with the path walkers when it was run with `paths: true`
600
+ * @returns the accessors
601
+ */
602
+ function apsp(c, r) {
603
+ const pair = (a, b) => {
604
+ const i = lookup(c, a);
605
+ const j = lookup(c, b);
606
+ return i === undefined || j === undefined ? undefined : [i, j];
607
+ };
608
+ return {
609
+ hasNegativeWeightCycle: r.hasNegativeCycle,
610
+ distance: (a, b) => {
611
+ const p = pair(a, b);
612
+ return p === undefined ? Infinity : r.dist[p[0] * r.n + p[1]];
613
+ },
614
+ path: (a, b) => {
615
+ const { pathTo, pathEdges } = r;
616
+ if (pathTo === undefined || pathEdges === undefined) {
617
+ // the same text the CPU result throws, so the message does not depend on the backend
618
+ throw new Error("allPairsShortestPath: pass paths: true to walk shortest paths");
619
+ }
620
+ const p = pair(a, b);
621
+ return p === undefined ? c.cy.collection() : pathOf(c, pathTo(p[0], p[1]), pathEdges(p[0], p[1]));
622
+ },
623
+ };
624
+ }
625
+ /**
626
+ * A PageRank result from scores.
627
+ * @param c - the context
628
+ * @param r - the run
629
+ * @returns the result
630
+ */
631
+ function ranked(c, r) {
632
+ return scored(c, r.scores, { rank: accessor(c, r.scores), iterations: r.iterations, converged: r.converged });
633
+ }
634
+ /**
635
+ * A label-propagation partition. The GPU result has no `iterations` / `converged` (the WebGPU label propagation
636
+ * does not report them), so both are undefined when the GPU ran.
637
+ * @param c - the context
638
+ * @param r - the run
639
+ * @returns the partition
640
+ */
641
+ function propagated(c, r) {
642
+ return partition(c, r.labels, { iterations: r.iterations, converged: r.converged });
643
+ }
644
+ /**
645
+ * The `topK` of a candidates method with the meaning it has on the prediction methods: a positive count keeps that
646
+ * many, 0 or less keeps every candidate (@graphty/algorithms slices with it, so 0 would keep none and -1 drop one).
647
+ * @param o - the options
648
+ * @param o.topK - the caller's value
649
+ * @returns the value for @graphty/algorithms
650
+ */
651
+ function topK(o) {
652
+ return o.topK !== undefined && o.topK <= 0 ? Infinity : o.topK;
653
+ }
654
+ /**
655
+ * A matching result.
656
+ * @param c - the context
657
+ * @param r - the matching
658
+ * @param r.matching - partner per node index
659
+ * @param r.size - matched pairs
660
+ * @returns mate accessor and size
661
+ */
662
+ function matched(c, r) {
663
+ const mate = new Uint32Array(c.s.nodeCount).fill(INVALID_INDEX);
664
+ r.matching.forEach((m, i) => {
665
+ if (m !== INVALID_INDEX) {
666
+ mate[i] = m;
667
+ mate[m] = i;
668
+ }
669
+ });
670
+ return {
671
+ size: r.size,
672
+ mate: (ref) => {
673
+ const i = lookup(c, ref);
674
+ return i === undefined || mate[i] === INVALID_INDEX ? undefined : c.cs.nodes[mate[i]];
675
+ },
676
+ };
677
+ }
678
+ /**
679
+ * The best level of a Girvan-Newman run.
680
+ * @param modularities - modularity per level
681
+ * @returns the level index with the highest modularity
682
+ */
683
+ function bestLevel(modularities) {
684
+ let best = 0;
685
+ for (let k = 1; k < modularities.length; k++) {
686
+ if (modularities[k] > modularities[best]) {
687
+ best = k;
688
+ }
689
+ }
690
+ return best;
691
+ }
692
+ // ---------------------------------------------------------------------------------------------------------------
693
+ // The algorithms. Key = the @graphty/algorithms name; registered as "graphty" + the key with a capital.
694
+ const IMPLS = {
695
+ // Traversal and paths
696
+ breadthFirstSearch: (c, o) => {
697
+ const target = indexOf(c.cs, o.goal, `${c.name}: goal`);
698
+ return then(c.call("breadthFirstSearch", c.s, req(c, o.root, "root"), { ...c.rest, target }), (r) => search(c, r, target));
699
+ },
700
+ directionOptimizedBfs: (c, o) => search(c, directionOptimizedBfs(c.s, req(c, o.root, "root"), c.rest), undefined),
701
+ depthFirstSearch: (c, o) => {
702
+ const target = indexOf(c.cs, o.goal, `${c.name}: goal`);
703
+ // a post-order walk cannot stop at the goal, so it would silently visit every node
704
+ if (target !== undefined && o.order === "post") {
705
+ throw new Error(`${c.name}: goal stops only a pre-order walk; leave it out with order "post"`);
706
+ }
707
+ return search(c, depthFirstSearch(c.s, req(c, o.root, "root"), { ...c.rest, target }), target);
708
+ },
709
+ hasCycle: (c, _o = {}) => hasCycle(c.s),
710
+ topologicalSort: (c, _o = {}) => {
711
+ const order = topologicalSort(c.s);
712
+ return order === null ? null : nodesAt(c, order);
713
+ },
714
+ dijkstra: (c, o) => {
715
+ nonNegative(c);
716
+ return then(c.call("sssp", c.s, req(c, o.root, "root"), c.rest), (r) => paths(c, r));
717
+ },
718
+ bellmanFord: (c, o) => then(c.call("bellmanFord", c.s, req(c, o.root, "root"), c.rest), (r) => ({
719
+ ...paths(c, r),
720
+ hasNegativeWeightCycle: r.hasNegativeCycle,
721
+ })),
722
+ bidirectionalDijkstra: (c, o) => {
723
+ nonNegative(c);
724
+ const r = bidirectionalDijkstra(c.s, req(c, o.root, "root"), req(c, o.goal, "goal"), c.rest);
725
+ return { found: r.path.length > 0, distance: r.distance, path: pathOf(c, r.path, r.edges) };
726
+ },
727
+ aStar: (c, o) => {
728
+ const h = o.heuristic;
729
+ nonNegative(c);
730
+ const r = astar(c.s, req(c, o.root, "root"), req(c, o.goal, "goal"), h === undefined ? () => 0 : (u) => h(c.cs.nodes[u]), c.rest);
731
+ return { found: r.path.length > 0, distance: r.distance, path: pathOf(c, r.path, r.edges) };
732
+ },
733
+ allPairsShortestPath: (c, _o = {}) => then(c.call("allPairsShortestPath", c.s, c.rest), (r) => apsp(c, r)),
734
+ // Centrality
735
+ degreeCentrality: (c, _o = {}) => {
736
+ const v = degreeCentrality(c.s, c.rest);
737
+ return scored(c, v, { degree: accessor(c, v) });
738
+ },
739
+ degrees: (c, _o = {}) => {
740
+ const r = degrees(c.s);
741
+ if (!c.s.directed) {
742
+ // read both ways, an edge enters and leaves each end: both accessors give the degree (@graphty/algorithms'
743
+ // degrees counts by source and target whatever the direction)
744
+ const degree = Float64Array.from(r.inDegree, (d, i) => d + r.outDegree[i]);
745
+ return { indegree: accessor(c, degree), outdegree: accessor(c, degree) };
746
+ }
747
+ return { indegree: accessor(c, r.inDegree), outdegree: accessor(c, r.outDegree) };
748
+ },
749
+ pageRank: (c, o = {}) => {
750
+ const initialRanks = o.initialRanks && vectorOf(c, o.initialRanks, "initialRanks");
751
+ return then(c.call("pageRank", c.s, { ...c.rest, initialRanks, weighted: c.weighted }), (r) => ranked(c, r));
752
+ },
753
+ personalizedPageRank: (c, o) => {
754
+ const p = teleportOf(c, o.personalization);
755
+ const initialRanks = o.initialRanks && vectorOf(c, o.initialRanks, "initialRanks");
756
+ return then(c.call("personalizedPageRank", c.s, p, { ...c.rest, initialRanks, weighted: c.weighted }), (r) => ranked(c, r));
757
+ },
758
+ deltaPageRank: (c, o = {}) => {
759
+ // The engines keep state for `update()` over the same frozen snapshot; a changed Cytoscape graph is a new
760
+ // snapshot, so only the one-shot computation is offered.
761
+ const personalization = o.personalization === undefined ? undefined : teleportOf(c, o.personalization);
762
+ // PriorityDeltaPageRank's maxIterations counts processed nodes, so the pinned 100 rounds would stop it long
763
+ // before it converges on any but a tiny graph: without the caller's own budget it runs until no delta is left
764
+ const budget = o.priority === true && o.maxIterations === undefined ? { maxIterations: Infinity } : {};
765
+ const opts = { ...c.rest, ...budget, personalization, weighted: c.weighted };
766
+ const v = o.priority === true
767
+ ? new PriorityDeltaPageRank(c.s).computeWithPriority(opts)
768
+ : new DeltaPageRank(c.s).compute(opts);
769
+ // ponytail: @graphty/algorithms' DeltaPageRank overflows to NaN at a large maxIterations; refuse it here
770
+ // until that engine converges (then this check never fires)
771
+ if (v.some((x) => !Number.isFinite(x))) {
772
+ throw new RangeError(`${c.name}: the ranks overflowed (maxIterations ${String(c.rest.maxIterations)} is too high for this graph); ` +
773
+ "lower maxIterations, or pass priority: true");
774
+ }
775
+ return scored(c, v, { rank: accessor(c, v) });
776
+ },
777
+ eigenvectorCentrality: (c, o = {}) => {
778
+ const startVector = o.startVector && vectorOf(c, o.startVector, "startVector");
779
+ return then(c.call("eigenvectorCentrality", c.s, { ...c.rest, startVector }), (r) => scored(c, r.scores, { iterations: r.iterations, converged: r.converged }));
780
+ },
781
+ katzCentrality: (c, _o = {}) => then(c.call("katzCentrality", c.s, { ...c.rest, weighted: c.weighted }), (r) => scored(c, r.scores, { iterations: r.iterations, converged: r.converged })),
782
+ hits: (c, _o = {}) => then(c.call("hits", c.s, { ...c.rest, weighted: c.weighted }), (r) => scored(c, r.authorities, {
783
+ hub: accessor(c, r.hubs),
784
+ authority: accessor(c, r.authorities),
785
+ iterations: r.iterations,
786
+ converged: r.converged,
787
+ })),
788
+ closenessCentrality: (c, o = {}) => {
789
+ const sources = o.sources === undefined ? undefined : indicesOf(c.cs, o.sources);
790
+ return then(c.call("closenessCentrality", c.s, { ...c.rest, sources, weighted: c.weighted }), (r) => scored(c, r.scores, { closeness: accessor(c, r.scores) }));
791
+ },
792
+ nodeClosenessCentrality: (c, o) => nodeClosenessCentrality(c.s, req(c, o.root, "root"), { ...c.rest, weighted: c.weighted }),
793
+ betweennessCentrality: (c, o = {}) => {
794
+ const sources = o.sources === undefined ? undefined : indicesOf(c.cs, o.sources);
795
+ return then(c.call("betweennessCentrality", c.s, { ...c.rest, sources }), (r) => {
796
+ // Cytoscape's `betweenness` counts ordered pairs, so on an undirected graph it is twice the NetworkX
797
+ // convention `score` follows. The result does not say which scale it is in, so the factor is rebuilt here
798
+ // from the rules in @graphty/algorithms' BetweennessOptions.normalized.
799
+ const n = c.s.nodeCount;
800
+ const pairs = o.endpoints === true ? n * (n - 1) : (n - 1) * (n - 2);
801
+ const unordered = c.s.directed ? 1 : 2;
802
+ const toOrderedPairs = o.normalized === true && pairs > 0 ? pairs : unordered;
803
+ const raw = Float64Array.from(r.scores, (x) => x * toOrderedPairs);
804
+ const max = raw.reduce((m, x) => Math.max(m, x), 0);
805
+ return scored(c, r.scores, {
806
+ betweenness: accessor(c, raw),
807
+ betweennessNormalized: accessor(c, raw.map((x) => (max === 0 ? 0 : x / max))),
808
+ });
809
+ });
810
+ },
811
+ edgeBetweennessCentrality: (c, o = {}) => {
812
+ const sources = o.sources === undefined ? undefined : indicesOf(c.cs, o.sources);
813
+ return then(c.call("edgeBetweennessCentrality", c.s, { ...c.rest, sources }), (r) => scored(c, r.scores, {}, true));
814
+ },
815
+ // Components and structure
816
+ connectedComponents: (c, _o = {}) => then(c.call("connectedComponents", c.s), (r) => partition(c, r.labels, {})),
817
+ weaklyConnectedComponents: (c, _o = {}) => then(c.call("weaklyConnectedComponents", c.s), (r) => partition(c, r.labels, {})),
818
+ stronglyConnectedComponents: (c, _o = {}) => partition(c, stronglyConnectedComponents(c.s).labels, {}),
819
+ condensation: (c, _o = {}) => {
820
+ const r = condensation(c.s);
821
+ return partition(c, r.components.labels, { condensed: r.condensed });
822
+ },
823
+ kCoreDecomposition: (c, _o = {}) => {
824
+ const r = kCoreDecomposition(c.s);
825
+ const core = (k) => nodesAt(c, Array.from(r.coreness.keys()).filter((i) => r.coreness[i] >= k));
826
+ return scored(c, r.coreness, { maxCore: r.maxCore, core });
827
+ },
828
+ triangleCount: (c, _o = {}) => then(c.call("triangleCount", c.s), (r) => scored(c, r.perNode, {
829
+ coefficient: accessor(c, r.coefficient),
830
+ total: r.total,
831
+ transitivity: r.transitivity,
832
+ })),
833
+ isBipartite: (c, _o = {}) => {
834
+ const r = isBipartite(c.s, c.rest);
835
+ if (r.sides === null) {
836
+ const empty = c.cy.collection();
837
+ return { bipartite: false, partitionFirst: empty, partitionSecond: empty };
838
+ }
839
+ const { partitionFirst, partitionSecond } = cutOf(c, r.sides, [], 0);
840
+ return { bipartite: true, partitionFirst, partitionSecond };
841
+ },
842
+ isGraphIsomorphic: (c, o) => {
843
+ const other = otherOf(c, o);
844
+ const r = isGraphIsomorphic(c.s, other.snapshot, isoOptions(c, other, o));
845
+ const m = r.mapping;
846
+ return { isomorphic: r.isomorphic, mapping: m === null ? () => undefined : mapper(c, other, m) };
847
+ },
848
+ findAllIsomorphisms: (c, o) => {
849
+ const other = otherOf(c, o);
850
+ return findAllIsomorphisms(c.s, other.snapshot, isoOptions(c, other, o)).map((m) => mapper(c, other, m));
851
+ },
852
+ modularity: (c, o) => modularity(c.s, labelsOf(c, o.clusters, "clusters"), c.rest),
853
+ // Community detection and clustering
854
+ louvain: (c, _o = {}) => {
855
+ const r = louvain(c.s, c.rest);
856
+ return partition(c, r.labels, { modularity: r.modularity, iterations: r.iterations });
857
+ },
858
+ leiden: (c, _o = {}) => {
859
+ const r = leiden(c.s, c.rest);
860
+ return partition(c, r.labels, { modularity: r.modularity, iterations: r.iterations });
861
+ },
862
+ labelPropagation: (c, _o = {}) => then(c.call("labelPropagation", c.s, { ...c.rest, weighted: c.weighted }), (r) => propagated(c, r)),
863
+ labelPropagationSynchronous: (c, _o = {}) => then(c.call("labelPropagationSynchronous", c.s, { ...c.rest, weighted: c.weighted }), (r) => propagated(c, r)),
864
+ labelPropagationSemiSupervised: (c, o) => {
865
+ const r = labelPropagationSemiSupervised(c.s, labelsOf(c, o.seeds, "seeds"), {
866
+ ...c.rest,
867
+ weighted: c.weighted,
868
+ });
869
+ return partition(c, r.labels, { iterations: r.iterations, converged: r.converged });
870
+ },
871
+ girvanNewman: (c, _o = {}) => {
872
+ const r = girvanNewman(c.s, c.rest);
873
+ const quiet = { ...c, field: undefined };
874
+ const level = (k) => partition(quiet, r.levels[k] ?? [], {});
875
+ const best = bestLevel(r.modularity);
876
+ return partition(c, r.levels[best] ?? [], {
877
+ modularity: r.modularity[best] ?? Number.NaN,
878
+ levels: r.levels.length,
879
+ level,
880
+ modularities: Array.from(r.modularity),
881
+ });
882
+ },
883
+ markovClustering: (c, _o = {}) => {
884
+ const r = markovClustering(c.s, c.rest);
885
+ return partition(c, r.labels, { iterations: r.iterations, converged: r.converged });
886
+ },
887
+ spectralClustering: (c, o) => {
888
+ // required by the type, checked for callers without types
889
+ if (o.k === undefined) {
890
+ throw new Error(`${c.name}: the k option is required`);
891
+ }
892
+ const r = spectralClustering(c.s, c.rest);
893
+ return partition(c, r.labels, { converged: r.converged, eigenvalues: r.eigenvalues });
894
+ },
895
+ hierarchicalClustering: (c, _o = {}) => {
896
+ const r = hierarchicalClustering(c.s, c.rest);
897
+ const quiet = { ...c, field: undefined };
898
+ const cut = (h) => {
899
+ const labels = new Uint32Array(c.s.nodeCount).fill(INVALID_INDEX);
900
+ r.cut(h).forEach((group, k) => {
901
+ for (const i of group) {
902
+ labels[i] = k;
903
+ }
904
+ });
905
+ return partition(quiet, labels, {});
906
+ };
907
+ const cutAt = (by) => partition(quiet, r.cutAt(by).labels, {});
908
+ return { merges: r.left.length, cut, cutAt };
909
+ },
910
+ teraHAC: (c, _o = {}) => {
911
+ const r = teraHAC(c.s, c.rest);
912
+ return partition(c, r.labels, { merges: r.merges });
913
+ },
914
+ grsbm: (c, _o = {}) => partition(c, grsbm(c.s, { ...c.rest, weighted: c.weighted }).labels, {}),
915
+ syncClustering: (c, o) => {
916
+ // required by the type, checked for callers without types
917
+ if (o.numClusters === undefined) {
918
+ throw new Error(`${c.name}: the numClusters option is required`);
919
+ }
920
+ const r = syncClustering(c.s, c.rest);
921
+ return partition(c, r.labels, { loss: r.loss, iterations: r.iterations, converged: r.converged });
922
+ },
923
+ // Spanning trees, flow, cuts, matching
924
+ kruskalMST: (c, _o = {}) => {
925
+ const r = kruskalMST(c.s);
926
+ return Object.assign(c.cs.nodes.union(edgesAt(c, r.edges)), { totalWeight: r.totalWeight });
927
+ },
928
+ primMST: (c, o = {}) => {
929
+ const start = indexOf(c.cs, o.root, `${c.name}: root`);
930
+ const r = primMST(c.s, { ...c.rest, start });
931
+ // a run that does not throw spans every node: the graph is connected, or `forest` grew a tree in every
932
+ // component (an isolated node is a tree of its own)
933
+ return Object.assign(c.cs.nodes.union(edgesAt(c, r.edges)), { totalWeight: r.totalWeight });
934
+ },
935
+ maxFlow: (c, o) => {
936
+ const r = maxFlow(c.s, req(c, o.source, "source"), req(c, o.sink, "sink"), c.rest);
937
+ return { ...cutOf(c, r.sourceSide, r.cutEdges, r.maxFlow), flow: accessor(c, r.flow, true) };
938
+ },
939
+ minSTCut: (c, o) => minCut(c, minSTCut(c.s, req(c, o.source, "source"), req(c, o.sink, "sink"), c.rest)),
940
+ stoerWagner: (c, _o = {}) => minCut(c, stoerWagner(c.s)),
941
+ kargerMinCut: (c, _o = {}) => minCut(c, kargerMinCut(c.s, c.rest)),
942
+ maximumBipartiteMatching: (c, o = {}) => matched(c, maximumBipartiteMatching(c.s, { ...c.rest, left: maskOf(c, o.left), right: maskOf(c, o.right) })),
943
+ greedyBipartiteMatching: (c, o = {}) => matched(c, greedyBipartiteMatching(c.s, { ...c.rest, left: maskOf(c, o.left), right: maskOf(c, o.right) })),
944
+ // Link prediction. `directed` also selects the directed score (out-neighbours of u against in-neighbours of v).
945
+ commonNeighborsScore: (c, o) => commonNeighborsScore(c.s, req(c, o.source, "source"), req(c, o.target, "target"), { directed: c.s.directed }),
946
+ adamicAdarScore: (c, o) => adamicAdarScore(c.s, req(c, o.source, "source"), req(c, o.target, "target"), { directed: c.s.directed }),
947
+ commonNeighborsPrediction: (c, _o = {}) => links(c, commonNeighborsPrediction(c.s, { ...c.rest, directed: c.s.directed, uniquePairs: true })),
948
+ adamicAdarPrediction: (c, _o = {}) => links(c, adamicAdarPrediction(c.s, { ...c.rest, directed: c.s.directed, uniquePairs: true })),
949
+ commonNeighborsForPairs: (c, o) => Array.from(commonNeighborsForPairs(c.s, pairsOf(c, o.pairs, "pairs"), { directed: c.s.directed })),
950
+ adamicAdarForPairs: (c, o) => Array.from(adamicAdarForPairs(c.s, pairsOf(c, o.pairs, "pairs"), { directed: c.s.directed })),
951
+ topCandidatesForNode: (c, o) => {
952
+ const candidates = o.candidates === undefined ? undefined : indicesOf(c.cs, o.candidates);
953
+ return links(c, getTopCandidatesForNode(c.s, req(c, o.root, "root"), {
954
+ ...c.rest,
955
+ topK: topK(o),
956
+ candidates,
957
+ directed: c.s.directed,
958
+ }));
959
+ },
960
+ topAdamicAdarCandidatesForNode: (c, o) => {
961
+ const candidates = o.candidates === undefined ? undefined : indicesOf(c.cs, o.candidates);
962
+ return links(c, getTopAdamicAdarCandidatesForNode(c.s, req(c, o.root, "root"), {
963
+ ...c.rest,
964
+ topK: topK(o),
965
+ candidates,
966
+ directed: c.s.directed,
967
+ }));
968
+ },
969
+ evaluateCommonNeighbors: (c, o) => evaluateCommonNeighbors(c.s, pairsOf(c, o.edges, "edges"), pairsOf(c, o.nonEdges, "nonEdges"), {
970
+ directed: c.s.directed,
971
+ }),
972
+ evaluateAdamicAdar: (c, o) => evaluateAdamicAdar(c.s, pairsOf(c, o.edges, "edges"), pairsOf(c, o.nonEdges, "nonEdges"), {
973
+ directed: c.s.directed,
974
+ }),
975
+ compareAdamicAdarWithCommonNeighbors: (c, o) => {
976
+ // both halves with `directed` (@graphty/algorithms' compareAdamicAdarWithCommonNeighbors drops it for the
977
+ // common-neighbors half), so each matches the method of its own name
978
+ const edges = pairsOf(c, o.edges, "edges");
979
+ const nonEdges = pairsOf(c, o.nonEdges, "nonEdges");
980
+ const opts = { directed: c.s.directed };
981
+ return {
982
+ adamicAdar: evaluateAdamicAdar(c.s, edges, nonEdges, opts),
983
+ commonNeighbors: evaluateCommonNeighbors(c.s, edges, nonEdges, opts),
984
+ };
985
+ },
986
+ };
987
+ /**
988
+ * The method name of an algorithm.
989
+ * @param key - the @graphty/algorithms name
990
+ * @returns "graphty" + the name with a capital
991
+ */
992
+ function methodName(key) {
993
+ return `graphty${key.charAt(0).toUpperCase()}${key.slice(1)}`;
994
+ }
995
+ /** Every synchronous method name `cytoscape.use(graphtyCytoscape)` registers on collections and the core. */
996
+ export const ALGORITHM_NAMES = Object.keys(IMPLS).map(methodName);
997
+ // The algorithms whose implementation goes through `c.call`, i.e. those the dispatcher can send to the GPU.
998
+ const ASYNC_KEYS = [
999
+ "breadthFirstSearch",
1000
+ "dijkstra",
1001
+ "bellmanFord",
1002
+ "allPairsShortestPath",
1003
+ "pageRank",
1004
+ "personalizedPageRank",
1005
+ "eigenvectorCentrality",
1006
+ "katzCentrality",
1007
+ "hits",
1008
+ "closenessCentrality",
1009
+ "betweennessCentrality",
1010
+ "edgeBetweennessCentrality",
1011
+ "connectedComponents",
1012
+ "weaklyConnectedComponents",
1013
+ "triangleCount",
1014
+ "labelPropagation",
1015
+ "labelPropagationSynchronous",
1016
+ ];
1017
+ /** Every `...Async` method name: the algorithms the WebGPU accelerator can run. */
1018
+ export const ASYNC_ALGORITHM_NAMES = ASYNC_KEYS.map((k) => `${methodName(k)}Async`);
1019
+ /**
1020
+ * Runs one algorithm over a collection.
1021
+ * @param eles - the collection
1022
+ * @param key - the algorithm
1023
+ * @param options - the caller's options
1024
+ * @param algos - the CPU functions (a synchronous run) or the dispatcher (an asynchronous one)
1025
+ * @returns the algorithm's result (a promise through the dispatcher) and the snapshot it ran on
1026
+ */
1027
+ function run(eles, key, options, algos) {
1028
+ const name = methodName(key);
1029
+ // a Cytoscape built-in takes some arguments positionally (kruskal(weightFn)); one ignored here would run unweighted
1030
+ if (typeof options !== "object" || options === null || Array.isArray(options)) {
1031
+ throw new TypeError(`${name}: takes one options object; pass a weight function as { weight: fn }`);
1032
+ }
1033
+ if (options.weight !== undefined && UNWEIGHTED.has(key)) {
1034
+ throw new Error(`${name}: this algorithm reads no edge weights; remove the weight option`);
1035
+ }
1036
+ if (options.field !== undefined && NO_FIELD.has(key)) {
1037
+ throw new Error(`${name}: this algorithm has no per-element value to write; remove the field option`);
1038
+ }
1039
+ const known = OPTION_NAMES[name] ?? [];
1040
+ for (const [k, v] of Object.entries(options)) {
1041
+ if (v !== undefined && !known.includes(k)) {
1042
+ throw new Error(`${name}: unknown option ${k}; the options are ${known.join(", ")}`);
1043
+ }
1044
+ }
1045
+ const directed = options.directed ?? DIRECTED.has(key);
1046
+ // degree and eigenvector centrality read mode only on a directed graph; ignored, it would give total degree
1047
+ const { mode } = options;
1048
+ if ((mode === "in" || mode === "out") && !directed) {
1049
+ throw new Error(`${name}: mode: "${mode}" needs directed: true`);
1050
+ }
1051
+ const cs = toSnapshot(eles, { directed, weight: options.weight });
1052
+ const rest = {
1053
+ ...DEFAULTS[key],
1054
+ ...Object.fromEntries(Object.entries(options).filter(([k, v]) => !ADAPTER_KEYS.has(k) && v !== undefined)),
1055
+ };
1056
+ const c = {
1057
+ name,
1058
+ cy: coreOf(eles),
1059
+ cs,
1060
+ s: cs.snapshot,
1061
+ field: options.field,
1062
+ weighted: options.weight !== undefined,
1063
+ rest,
1064
+ call: (k, ...args) => algos[k](...args),
1065
+ };
1066
+ const fail = (error) => {
1067
+ throw named(error, key);
1068
+ };
1069
+ let result;
1070
+ try {
1071
+ result = IMPLS[key](c, options);
1072
+ }
1073
+ catch (error) {
1074
+ fail(error);
1075
+ }
1076
+ return { result: result instanceof Promise ? result.catch(fail) : result, snapshot: cs.snapshot };
1077
+ }
1078
+ /**
1079
+ * An algorithm's error in a Cytoscape user's terms: it starts with the method name, and a graph of the wrong
1080
+ * direction is told which option to change (the @graphty/algorithms message names its own functions and snapshot
1081
+ * methods). The error object, its class and its `code` are kept.
1082
+ * @param error - what the algorithm threw
1083
+ * @param key - the algorithm
1084
+ * @returns the error to throw
1085
+ */
1086
+ function named(error, key) {
1087
+ if (!(error instanceof Error)) {
1088
+ return error;
1089
+ }
1090
+ const name = methodName(key);
1091
+ const { code } = error;
1092
+ if (code === "E_NEEDS_UNDIRECTED") {
1093
+ const instead = key === "connectedComponents" ? ", or call graphtyWeaklyConnectedComponents" : "";
1094
+ error.message = `${name}: needs an undirected graph; leave out directed: true${instead}`;
1095
+ }
1096
+ else if (code === "E_NEEDS_DIRECTED") {
1097
+ error.message = `${name}: needs a directed graph; ${DIRECTED.has(key) ? "leave out directed: false" : "pass directed: true"}`;
1098
+ }
1099
+ else if (!error.message.startsWith(`${name}:`)) {
1100
+ error.message = `${name}: ${error.message}`;
1101
+ }
1102
+ return error;
1103
+ }
1104
+ /**
1105
+ * Runs an algorithm synchronously on the CPU.
1106
+ * @param eles - the collection
1107
+ * @param key - the algorithm
1108
+ * @param options - the caller's options
1109
+ * @returns the result
1110
+ */
1111
+ function runSync(eles, key, options = {}) {
1112
+ checkGpuMode(methodName(key), options?.gpu);
1113
+ if (options.gpu === "require") {
1114
+ const name = methodName(key);
1115
+ throw new Error(ASYNC_KEYS.includes(key)
1116
+ ? `${name}: runs on the CPU; call ${name}Async for the GPU`
1117
+ : `${name}: has no GPU implementation and runs only on the CPU; leave out gpu: "require"`);
1118
+ }
1119
+ return run(eles, key, options, CPU).result;
1120
+ }
1121
+ /**
1122
+ * Runs an algorithm through the @graphty/algorithms dispatcher, on the core's GPU when it has one, and reports
1123
+ * which implementation answered. A GPU failure after the run started is thrown, never finished on the CPU.
1124
+ * @param eles - the collection
1125
+ * @param key - the algorithm
1126
+ * @param options - the caller's options
1127
+ * @returns the result with `backend`
1128
+ */
1129
+ async function runAsync(eles, key, options = {}) {
1130
+ checkGpuMode(`${methodName(key)}Async`, options?.gpu);
1131
+ const cy = coreOf(eles);
1132
+ const d = await gpuFor(cy, options.gpu);
1133
+ // destroyed while the device was acquired: its device is disposed and its elements are gone with it
1134
+ if (cy.destroyed()) {
1135
+ throw coreDestroyedError();
1136
+ }
1137
+ const rec = d.gpu === null ? null : recording(d.gpu.accelerator);
1138
+ warnIfFixable(d, eles.nodes().length);
1139
+ const { result, snapshot } = run(eles, key, options, accelerated(rec?.accelerator ?? null));
1140
+ // a data change during the run (another run writing its field, say) must not release what this run reads
1141
+ const unhold = hold(snapshot);
1142
+ let value;
1143
+ try {
1144
+ value = (await result);
1145
+ }
1146
+ catch (e) {
1147
+ // destroyed mid-run, the device fails with "GpuContext is disposed"; the cause is the destroy
1148
+ throw cy.destroyed() ? coreDestroyedError() : e;
1149
+ }
1150
+ finally {
1151
+ unhold();
1152
+ if (d.gpu !== null && typeof options.weight === "function") {
1153
+ // a weight function bypasses the snapshot cache, so nothing else would release this upload
1154
+ d.gpu.accelerator.release(snapshot);
1155
+ }
1156
+ }
1157
+ const backend = backendOf(d, rec?.used() ?? false, "the options or the graph need the CPU implementation");
1158
+ return Object.assign(value, { backend });
1159
+ }
1160
+ /**
1161
+ * Registers every algorithm as a collection method and a core method named "graphty<Name>".
1162
+ * @param cytoscape - the cytoscape function
1163
+ */
1164
+ export function registerAlgorithms(cytoscape) {
1165
+ for (const key of Object.keys(IMPLS)) {
1166
+ const name = methodName(key);
1167
+ cytoscape("collection", name, function (options) {
1168
+ return runSync(this, key, options);
1169
+ });
1170
+ cytoscape("core", name, function (options) {
1171
+ return runSync(this.elements(), key, options);
1172
+ });
1173
+ }
1174
+ for (const key of ASYNC_KEYS) {
1175
+ const name = `${methodName(key)}Async`;
1176
+ cytoscape("collection", name, function (options) {
1177
+ return runAsync(this, key, options);
1178
+ });
1179
+ cytoscape("core", name, function (options) {
1180
+ return runAsync(this.elements(), key, options);
1181
+ });
1182
+ }
1183
+ }
1184
+ //# sourceMappingURL=algorithms.js.map