@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.
- package/dist/algorithm-options.d.ts +5 -0
- package/dist/algorithm-options.d.ts.map +1 -0
- package/dist/algorithm-options.js +285 -0
- package/dist/algorithm-options.js.map +1 -0
- package/dist/algorithms.d.ts +489 -0
- package/dist/algorithms.d.ts.map +1 -0
- package/dist/algorithms.js +1184 -0
- package/dist/algorithms.js.map +1 -0
- package/dist/cdn/chunks/elements-DIunzJUl.js +1 -0
- package/dist/cdn/chunks/gpu-platform-browser-DnQwf3s0.js +2825 -0
- package/dist/cdn/chunks/index-C-jUF_Ve.js +1 -0
- package/dist/cdn/chunks/index-C4RZkds_.js +10 -0
- package/dist/cdn/chunks/io-D-Vr0j3W.js +269 -0
- package/dist/cdn/chunks/preload-helper-Crt_PPe8.js +1 -0
- package/dist/cdn/chunks/samples-Dja23zzq.js +1 -0
- package/dist/cdn/chunks/sigv4-BXwVbza3-DZCCNyQA.js +3 -0
- package/dist/cdn/chunks/sources-cypher-oj3S6pKb.js +5 -0
- package/dist/cdn/chunks/sources-ndex-BgZhxHsL.js +1 -0
- package/dist/cdn/chunks/sources-rest-E6On5P3z.js +1 -0
- package/dist/cdn/chunks/sources-sparql-BAxh1dw-.js +5 -0
- package/dist/cdn/cytoscape-extensions.js +1 -0
- package/dist/cdn/io.js +1 -0
- package/dist/cytoscape-extensions.bundle.js +2825 -0
- package/dist/elements.d.ts +69 -0
- package/dist/elements.d.ts.map +1 -0
- package/dist/elements.js +343 -0
- package/dist/elements.js.map +1 -0
- package/dist/gpu-platform-browser.d.ts +11 -0
- package/dist/gpu-platform-browser.d.ts.map +1 -0
- package/dist/gpu-platform-browser.js +22 -0
- package/dist/gpu-platform-browser.js.map +1 -0
- package/dist/gpu-platform-node.d.ts +15 -0
- package/dist/gpu-platform-node.d.ts.map +1 -0
- package/dist/gpu-platform-node.js +27 -0
- package/dist/gpu-platform-node.js.map +1 -0
- package/dist/gpu.d.ts +149 -0
- package/dist/gpu.d.ts.map +1 -0
- package/dist/gpu.js +312 -0
- package/dist/gpu.js.map +1 -0
- package/dist/graph-data.d.ts +101 -0
- package/dist/graph-data.d.ts.map +1 -0
- package/dist/graph-data.js +147 -0
- package/dist/graph-data.js.map +1 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +48 -0
- package/dist/index.js.map +1 -0
- package/dist/io.d.ts +111 -0
- package/dist/io.d.ts.map +1 -0
- package/dist/io.js +258 -0
- package/dist/io.js.map +1 -0
- package/dist/layouts.d.ts +207 -0
- package/dist/layouts.d.ts.map +1 -0
- package/dist/layouts.js +615 -0
- package/dist/layouts.js.map +1 -0
- package/dist/lazy.d.ts +21 -0
- package/dist/lazy.d.ts.map +1 -0
- package/dist/lazy.js +32 -0
- package/dist/lazy.js.map +1 -0
- package/dist/samples.d.ts +115 -0
- package/dist/samples.d.ts.map +1 -0
- package/dist/samples.js +237 -0
- package/dist/samples.js.map +1 -0
- package/dist/snapshot.d.ts +81 -0
- package/dist/snapshot.d.ts.map +1 -0
- package/dist/snapshot.js +225 -0
- package/dist/snapshot.js.map +1 -0
- package/dist/webgpu-provider.d.ts +25 -0
- package/dist/webgpu-provider.d.ts.map +1 -0
- package/dist/webgpu-provider.js +53 -0
- package/dist/webgpu-provider.js.map +1 -0
- 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
|