@graphty/cytoscape-extensions 0.0.0-placeholder.0 → 0.0.1
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/LICENSE +21 -0
- package/NOTICE +10 -0
- package/README.md +73 -7
- package/package.json +124 -14
- package/src/algorithm-options.ts +277 -0
- package/src/algorithms.ts +1911 -0
- package/src/elements.ts +357 -0
- package/src/gpu-platform-browser.ts +29 -0
- package/src/gpu-platform-node.ts +35 -0
- package/src/gpu.ts +407 -0
- package/src/graph-data.ts +165 -0
- package/src/index.ts +73 -0
- package/src/io.ts +205 -0
- package/src/layouts.ts +886 -0
- package/src/lazy.ts +35 -0
- package/src/samples.ts +273 -0
- package/src/snapshot.ts +281 -0
- package/src/webgpu-provider.ts +67 -0
|
@@ -0,0 +1,1911 @@
|
|
|
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
|
+
|
|
21
|
+
import {
|
|
22
|
+
accelerated,
|
|
23
|
+
type AcceleratedAlgorithms,
|
|
24
|
+
adamicAdarForPairs,
|
|
25
|
+
adamicAdarPrediction,
|
|
26
|
+
adamicAdarScore,
|
|
27
|
+
allPairsShortestPath,
|
|
28
|
+
type ApspOptions,
|
|
29
|
+
astar,
|
|
30
|
+
bellmanFord,
|
|
31
|
+
betweennessCentrality,
|
|
32
|
+
type BetweennessOptions,
|
|
33
|
+
bidirectionalDijkstra,
|
|
34
|
+
type BipartiteOptions,
|
|
35
|
+
breadthFirstSearch,
|
|
36
|
+
type CandidateOptions,
|
|
37
|
+
closenessCentrality,
|
|
38
|
+
type ClosenessOptions,
|
|
39
|
+
commonNeighborsForPairs,
|
|
40
|
+
commonNeighborsPrediction,
|
|
41
|
+
commonNeighborsScore,
|
|
42
|
+
condensation,
|
|
43
|
+
connectedComponents,
|
|
44
|
+
degreeCentrality,
|
|
45
|
+
type DegreeCentralityOptions,
|
|
46
|
+
degrees,
|
|
47
|
+
DeltaPageRank,
|
|
48
|
+
type DeltaPageRankComputeOptions,
|
|
49
|
+
depthFirstSearch,
|
|
50
|
+
dijkstra,
|
|
51
|
+
directionOptimizedBfs,
|
|
52
|
+
type DirectionOptimizedBfsOptions,
|
|
53
|
+
edgeBetweennessCentrality,
|
|
54
|
+
eigenvectorCentrality,
|
|
55
|
+
type EigenvectorOptions,
|
|
56
|
+
evaluateAdamicAdar,
|
|
57
|
+
evaluateCommonNeighbors,
|
|
58
|
+
findAllIsomorphisms,
|
|
59
|
+
getTopAdamicAdarCandidatesForNode,
|
|
60
|
+
getTopCandidatesForNode,
|
|
61
|
+
girvanNewman,
|
|
62
|
+
type GirvanNewmanOptions,
|
|
63
|
+
greedyBipartiteMatching,
|
|
64
|
+
grsbm,
|
|
65
|
+
type GrsbmOptions,
|
|
66
|
+
hasCycle,
|
|
67
|
+
hierarchicalClustering,
|
|
68
|
+
type HierarchicalOptions,
|
|
69
|
+
hits,
|
|
70
|
+
type HitsOptions,
|
|
71
|
+
isBipartite,
|
|
72
|
+
isGraphIsomorphic,
|
|
73
|
+
kargerMinCut,
|
|
74
|
+
type KargerOptions,
|
|
75
|
+
katzCentrality,
|
|
76
|
+
type KatzOptions,
|
|
77
|
+
kCoreDecomposition,
|
|
78
|
+
kruskalMST,
|
|
79
|
+
labelPropagation,
|
|
80
|
+
type LabelPropagationOptions,
|
|
81
|
+
labelPropagationSemiSupervised,
|
|
82
|
+
labelPropagationSynchronous,
|
|
83
|
+
leiden,
|
|
84
|
+
type LeidenOptions,
|
|
85
|
+
type LinkPredictionMetrics,
|
|
86
|
+
type LinkPredictionOptions,
|
|
87
|
+
type LinkPredictionResult,
|
|
88
|
+
louvain,
|
|
89
|
+
type LouvainOptions,
|
|
90
|
+
markovClustering,
|
|
91
|
+
type MarkovOptions,
|
|
92
|
+
maxFlow,
|
|
93
|
+
type MaxFlowOptions,
|
|
94
|
+
maximumBipartiteMatching,
|
|
95
|
+
type MinCutResult,
|
|
96
|
+
minSTCut,
|
|
97
|
+
modularity,
|
|
98
|
+
type ModularityOptions,
|
|
99
|
+
nodeClosenessCentrality,
|
|
100
|
+
pageRank,
|
|
101
|
+
type PageRankOptions,
|
|
102
|
+
personalizedPageRank,
|
|
103
|
+
primMST,
|
|
104
|
+
PriorityDeltaPageRank,
|
|
105
|
+
spectralClustering,
|
|
106
|
+
type SpectralOptions,
|
|
107
|
+
stoerWagner,
|
|
108
|
+
stronglyConnectedComponents,
|
|
109
|
+
syncClustering,
|
|
110
|
+
type SyncClusteringOptions,
|
|
111
|
+
teraHAC,
|
|
112
|
+
type TeraHacOptions,
|
|
113
|
+
topologicalSort,
|
|
114
|
+
triangleCount,
|
|
115
|
+
weaklyConnectedComponents,
|
|
116
|
+
} from "@graphty/algorithms";
|
|
117
|
+
import {
|
|
118
|
+
type DerivedGraph,
|
|
119
|
+
type F64,
|
|
120
|
+
type GraphSnapshot,
|
|
121
|
+
INVALID_INDEX,
|
|
122
|
+
makeMask,
|
|
123
|
+
maskSet,
|
|
124
|
+
maskTest,
|
|
125
|
+
type U32,
|
|
126
|
+
} from "@graphty/graph-format";
|
|
127
|
+
import type {
|
|
128
|
+
Collection,
|
|
129
|
+
CollectionArgument,
|
|
130
|
+
CollectionReturnValue,
|
|
131
|
+
Core,
|
|
132
|
+
EdgeCollection,
|
|
133
|
+
EdgeSingular,
|
|
134
|
+
NodeCollection,
|
|
135
|
+
NodeSingular,
|
|
136
|
+
} from "cytoscape";
|
|
137
|
+
|
|
138
|
+
import { OPTION_NAMES } from "./algorithm-options.js";
|
|
139
|
+
import { type Backend, backendOf, checkGpuMode, gpuFor, type GpuMode, recording, warnIfFixable } from "./gpu.js";
|
|
140
|
+
import {
|
|
141
|
+
coreOf,
|
|
142
|
+
type CytoscapeSnapshot,
|
|
143
|
+
hold,
|
|
144
|
+
indexOf,
|
|
145
|
+
indicesOf,
|
|
146
|
+
type NodeSelection,
|
|
147
|
+
type SnapshotOptions,
|
|
148
|
+
toSnapshot,
|
|
149
|
+
writeData,
|
|
150
|
+
} from "./snapshot.js";
|
|
151
|
+
|
|
152
|
+
/** A node or edge as a result accessor takes it: the element, or a selector whose first match is used. */
|
|
153
|
+
export type ElementRef = string | Collection | NodeCollection | EdgeCollection;
|
|
154
|
+
|
|
155
|
+
/** Options every "graphty*" algorithm takes. */
|
|
156
|
+
export interface AlgorithmOptions {
|
|
157
|
+
/** Read the edges as directed (source to target). Default false, except where an algorithm says otherwise. */
|
|
158
|
+
readonly directed?: boolean;
|
|
159
|
+
/** Edge data field holding the weight, or a function of the edge. Default: every edge weighs 1. */
|
|
160
|
+
readonly weight?: SnapshotOptions["weight"];
|
|
161
|
+
/**
|
|
162
|
+
* Write each element's value (what `score` or `cluster` returns) into `data(field)`. A method whose result has no
|
|
163
|
+
* per-element value (a path, a tree, a number, a list of predictions) throws when given it.
|
|
164
|
+
*/
|
|
165
|
+
readonly field?: string;
|
|
166
|
+
/**
|
|
167
|
+
* The `...Async` methods only: "auto" (default) runs on the GPU when the runtime has a usable WebGPU device,
|
|
168
|
+
* "off" runs on the CPU, "require" throws instead of running on the CPU when no device is available. The synchronous methods always run on the CPU and reject "require". Any other value throws a TypeError.
|
|
169
|
+
*/
|
|
170
|
+
readonly gpu?: GpuMode;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Per-element values, keyed by element. */
|
|
174
|
+
export interface ScoreResult {
|
|
175
|
+
/** The value of an element of the collection; undefined for one outside it. */
|
|
176
|
+
score(ele: ElementRef): number | undefined;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** A partition: an array of node collections, one per cluster, plus each node's cluster number. */
|
|
180
|
+
export type Partition<T = object> = NodeCollection[] &
|
|
181
|
+
T & {
|
|
182
|
+
/** The cluster number (its position in the array) of a node; undefined for one outside the collection. */
|
|
183
|
+
cluster(node: ElementRef): number | undefined;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
/** Paths from one root, in the shape of Cytoscape's `dijkstra`. */
|
|
187
|
+
export interface PathsResult {
|
|
188
|
+
/** Distance from the root; Infinity when unreachable. */
|
|
189
|
+
distanceTo(node: ElementRef): number;
|
|
190
|
+
/** The path from the root, alternating node, edge, node; empty when unreachable. */
|
|
191
|
+
pathTo(node: ElementRef): CollectionReturnValue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** A walk from one root, in the shape of Cytoscape's `bfs` / `dfs`. */
|
|
195
|
+
export interface SearchResult {
|
|
196
|
+
/** The visited nodes in visit order, each after the tree edge that reached it. */
|
|
197
|
+
readonly path: CollectionReturnValue;
|
|
198
|
+
/** The `goal` node when it was reached, else empty. */
|
|
199
|
+
readonly found: CollectionReturnValue;
|
|
200
|
+
/**
|
|
201
|
+
* Edges from the root along the walk's tree (the parent links): the shortest hop count for a breadth-first
|
|
202
|
+
* search, the tree depth for a depth-first search, which can be larger. Undefined when not visited.
|
|
203
|
+
*/
|
|
204
|
+
depth(node: ElementRef): number | undefined;
|
|
205
|
+
/** The node this one was reached from; undefined for the root and unvisited nodes. */
|
|
206
|
+
parent(node: ElementRef): NodeSingular | undefined;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** One path between two nodes, in the shape of Cytoscape's `aStar`. */
|
|
210
|
+
export interface PointPathResult {
|
|
211
|
+
/** Whether `goal` can be reached from `root`. */
|
|
212
|
+
readonly found: boolean;
|
|
213
|
+
/** The length of the path; Infinity when not found. */
|
|
214
|
+
readonly distance: number;
|
|
215
|
+
/** Alternating node, edge, node; empty when not found. */
|
|
216
|
+
readonly path: CollectionReturnValue;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* A cut, like the result of Cytoscape's `kargerStein` with `partition1` and `partition2` named `partitionFirst` and
|
|
221
|
+
* `partitionSecond`, plus `value` and without `components`.
|
|
222
|
+
*/
|
|
223
|
+
export interface CutResult {
|
|
224
|
+
/** The total weight (capacity) of the cut edges. */
|
|
225
|
+
readonly value: number;
|
|
226
|
+
/** The edges that cross the cut. */
|
|
227
|
+
readonly cut: EdgeCollection;
|
|
228
|
+
/** The nodes on one side: the source side for a flow or s-t cut. */
|
|
229
|
+
readonly partitionFirst: NodeCollection;
|
|
230
|
+
/** The nodes on the other side. */
|
|
231
|
+
readonly partitionSecond: NodeCollection;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** A predicted link. */
|
|
235
|
+
export interface PredictedLink {
|
|
236
|
+
/** One end of the proposed edge. */
|
|
237
|
+
readonly source: NodeSingular;
|
|
238
|
+
/** The other end. */
|
|
239
|
+
readonly target: NodeSingular;
|
|
240
|
+
/** The pair's score; higher is a more likely link. */
|
|
241
|
+
readonly score: number;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** A pair of nodes, each a selector or a collection. */
|
|
245
|
+
export type NodePair = readonly [NodeSelection, NodeSelection];
|
|
246
|
+
|
|
247
|
+
/** Held-out edges and non-edges for the link-prediction evaluations. */
|
|
248
|
+
interface EvaluateOptions extends AlgorithmOptions {
|
|
249
|
+
/** Node pairs that are edges (held out of the graph), which a good score ranks high. */
|
|
250
|
+
readonly edges: readonly NodePair[];
|
|
251
|
+
/** Node pairs that are not edges, which a good score ranks low. */
|
|
252
|
+
readonly nonEdges: readonly NodePair[];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** What every algorithm implementation gets. */
|
|
256
|
+
interface Ctx {
|
|
257
|
+
readonly name: string;
|
|
258
|
+
readonly cy: Core;
|
|
259
|
+
readonly cs: CytoscapeSnapshot;
|
|
260
|
+
readonly s: GraphSnapshot;
|
|
261
|
+
readonly field: string | undefined;
|
|
262
|
+
/** `weighted` for the algorithms that read the snapshot's weights only when asked. */
|
|
263
|
+
readonly weighted: boolean;
|
|
264
|
+
/** The caller's options without the adapter's own, over this package's pinned defaults (see DEFAULTS). */
|
|
265
|
+
readonly rest: Record<string, unknown>;
|
|
266
|
+
/**
|
|
267
|
+
* Runs one of the algorithms the GPU can answer: synchronously on the CPU for the plain methods, through the
|
|
268
|
+
* dispatcher of `@graphty/algorithms` (GPU or CPU, a promise) for the `...Async` methods.
|
|
269
|
+
*/
|
|
270
|
+
call<K extends GpuKey>(key: K, ...args: Parameters<Dispatch[K]>): Out<K> | Promise<Out<K>>;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** The @graphty/algorithms dispatcher's methods: the ones an accelerator can answer, by the dispatcher's names. */
|
|
274
|
+
type Dispatch = Omit<AcceleratedAlgorithms, "accelerator">;
|
|
275
|
+
type Out<K extends keyof Dispatch> = Awaited<ReturnType<Dispatch[K]>>;
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* The CPU functions behind the dispatcher methods the WebGPU accelerator implements (its `createAccelerator`
|
|
279
|
+
* list), under the dispatcher's names. The plain methods call these directly, so they stay synchronous.
|
|
280
|
+
*/
|
|
281
|
+
const CPU = {
|
|
282
|
+
breadthFirstSearch,
|
|
283
|
+
sssp: dijkstra,
|
|
284
|
+
bellmanFord,
|
|
285
|
+
allPairsShortestPath,
|
|
286
|
+
pageRank,
|
|
287
|
+
personalizedPageRank,
|
|
288
|
+
eigenvectorCentrality,
|
|
289
|
+
katzCentrality,
|
|
290
|
+
hits,
|
|
291
|
+
closenessCentrality,
|
|
292
|
+
betweennessCentrality,
|
|
293
|
+
edgeBetweennessCentrality,
|
|
294
|
+
connectedComponents,
|
|
295
|
+
weaklyConnectedComponents,
|
|
296
|
+
triangleCount,
|
|
297
|
+
labelPropagation,
|
|
298
|
+
labelPropagationSynchronous,
|
|
299
|
+
} satisfies Partial<{ [K in keyof Dispatch]: (...args: Parameters<Dispatch[K]>) => Out<K> }>;
|
|
300
|
+
|
|
301
|
+
type GpuKey = keyof typeof CPU;
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Applies a function to a value that is either plain (a CPU run) or a promise (a dispatcher run).
|
|
305
|
+
* @param x - the value or its promise
|
|
306
|
+
* @param f - the function
|
|
307
|
+
* @returns f(x), or its promise
|
|
308
|
+
*/
|
|
309
|
+
function then<T, U>(x: T | Promise<T>, f: (t: T) => U): U | Promise<U> {
|
|
310
|
+
return x instanceof Promise ? x.then(f) : f(x);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Options the adapter consumes and converts; everything else passes through to the algorithm.
|
|
314
|
+
const ADAPTER_KEYS = new Set([
|
|
315
|
+
"gpu",
|
|
316
|
+
"directed",
|
|
317
|
+
"weight",
|
|
318
|
+
"field",
|
|
319
|
+
"root",
|
|
320
|
+
"goal",
|
|
321
|
+
"target",
|
|
322
|
+
"source",
|
|
323
|
+
"sink",
|
|
324
|
+
"heuristic",
|
|
325
|
+
"personalization",
|
|
326
|
+
"seeds",
|
|
327
|
+
"left",
|
|
328
|
+
"right",
|
|
329
|
+
"clusters",
|
|
330
|
+
"pairs",
|
|
331
|
+
"edges",
|
|
332
|
+
"nonEdges",
|
|
333
|
+
"nodeMatch",
|
|
334
|
+
"edgeMatch",
|
|
335
|
+
"priority",
|
|
336
|
+
"candidates",
|
|
337
|
+
"sources",
|
|
338
|
+
"alive",
|
|
339
|
+
"startVector",
|
|
340
|
+
"initialRanks",
|
|
341
|
+
]);
|
|
342
|
+
|
|
343
|
+
// Algorithms that read no weights: a `weight` option would be silently ignored, so it throws.
|
|
344
|
+
export const UNWEIGHTED: ReadonlySet<string> = new Set([
|
|
345
|
+
"breadthFirstSearch",
|
|
346
|
+
"directionOptimizedBfs",
|
|
347
|
+
"depthFirstSearch",
|
|
348
|
+
"hasCycle",
|
|
349
|
+
"topologicalSort",
|
|
350
|
+
"degreeCentrality",
|
|
351
|
+
"degrees",
|
|
352
|
+
"eigenvectorCentrality",
|
|
353
|
+
"betweennessCentrality",
|
|
354
|
+
"edgeBetweennessCentrality",
|
|
355
|
+
"connectedComponents",
|
|
356
|
+
"weaklyConnectedComponents",
|
|
357
|
+
"stronglyConnectedComponents",
|
|
358
|
+
"condensation",
|
|
359
|
+
"kCoreDecomposition",
|
|
360
|
+
"triangleCount",
|
|
361
|
+
"isBipartite",
|
|
362
|
+
"isGraphIsomorphic",
|
|
363
|
+
"findAllIsomorphisms",
|
|
364
|
+
"hierarchicalClustering",
|
|
365
|
+
"teraHAC",
|
|
366
|
+
"syncClustering",
|
|
367
|
+
"maximumBipartiteMatching",
|
|
368
|
+
"greedyBipartiteMatching",
|
|
369
|
+
"commonNeighborsScore",
|
|
370
|
+
"adamicAdarScore",
|
|
371
|
+
"commonNeighborsPrediction",
|
|
372
|
+
"adamicAdarPrediction",
|
|
373
|
+
"commonNeighborsForPairs",
|
|
374
|
+
"adamicAdarForPairs",
|
|
375
|
+
"topCandidatesForNode",
|
|
376
|
+
"topAdamicAdarCandidatesForNode",
|
|
377
|
+
"evaluateCommonNeighbors",
|
|
378
|
+
"evaluateAdamicAdar",
|
|
379
|
+
"compareAdamicAdarWithCommonNeighbors",
|
|
380
|
+
]);
|
|
381
|
+
|
|
382
|
+
// Algorithms whose result has no per-element value to write: a `field` option would be silently ignored, so it throws.
|
|
383
|
+
export const NO_FIELD: ReadonlySet<string> = new Set([
|
|
384
|
+
"hasCycle",
|
|
385
|
+
"topologicalSort",
|
|
386
|
+
"bidirectionalDijkstra",
|
|
387
|
+
"aStar",
|
|
388
|
+
"allPairsShortestPath",
|
|
389
|
+
"degrees",
|
|
390
|
+
"nodeClosenessCentrality",
|
|
391
|
+
"isGraphIsomorphic",
|
|
392
|
+
"findAllIsomorphisms",
|
|
393
|
+
"modularity",
|
|
394
|
+
"hierarchicalClustering",
|
|
395
|
+
"kruskalMST",
|
|
396
|
+
"primMST",
|
|
397
|
+
"maximumBipartiteMatching",
|
|
398
|
+
"greedyBipartiteMatching",
|
|
399
|
+
"commonNeighborsScore",
|
|
400
|
+
"adamicAdarScore",
|
|
401
|
+
"commonNeighborsPrediction",
|
|
402
|
+
"adamicAdarPrediction",
|
|
403
|
+
"commonNeighborsForPairs",
|
|
404
|
+
"adamicAdarForPairs",
|
|
405
|
+
"topCandidatesForNode",
|
|
406
|
+
"topAdamicAdarCandidatesForNode",
|
|
407
|
+
"evaluateCommonNeighbors",
|
|
408
|
+
"evaluateAdamicAdar",
|
|
409
|
+
"compareAdamicAdarWithCommonNeighbors",
|
|
410
|
+
]);
|
|
411
|
+
|
|
412
|
+
// This package's own defaults, passed explicitly to @graphty/algorithms (and through it to the GPU), so a change of
|
|
413
|
+
// a library default does not change what a Cytoscape user gets. docs/reference/algorithms.md shows them; a caller's
|
|
414
|
+
// option overrides them. `weighted` is pinned separately: it is true exactly when the caller passed `weight`.
|
|
415
|
+
const POWER = { maxIterations: 100, tolerance: 1e-6 } as const;
|
|
416
|
+
const PAGERANK = { ...POWER, dampingFactor: 0.85 } as const;
|
|
417
|
+
export const DEFAULTS: Readonly<Record<string, Readonly<Record<string, unknown>>>> = {
|
|
418
|
+
pageRank: PAGERANK,
|
|
419
|
+
personalizedPageRank: PAGERANK,
|
|
420
|
+
deltaPageRank: PAGERANK,
|
|
421
|
+
eigenvectorCentrality: POWER,
|
|
422
|
+
hits: POWER,
|
|
423
|
+
katzCentrality: { ...POWER, alpha: 0.1, beta: 1 },
|
|
424
|
+
labelPropagation: { maxIterations: 100 },
|
|
425
|
+
labelPropagationSynchronous: { maxIterations: 100 },
|
|
426
|
+
labelPropagationSemiSupervised: { maxIterations: 100 },
|
|
427
|
+
allPairsShortestPath: { paths: true },
|
|
428
|
+
maxFlow: { algorithm: "edmonds-karp" },
|
|
429
|
+
minSTCut: { algorithm: "ford-fulkerson" },
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
// Algorithms defined only for directed graphs: `directed` defaults to true.
|
|
433
|
+
export const DIRECTED: ReadonlySet<string> = new Set([
|
|
434
|
+
"topologicalSort",
|
|
435
|
+
"stronglyConnectedComponents",
|
|
436
|
+
"condensation",
|
|
437
|
+
"deltaPageRank",
|
|
438
|
+
]);
|
|
439
|
+
|
|
440
|
+
// ---------------------------------------------------------------------------------------------------------------
|
|
441
|
+
// Conversions between elements and indices
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* A collection of elements, in the given order. Cytoscape accepts an element array at run time; its typings declare
|
|
445
|
+
* only element definitions, hence the cast.
|
|
446
|
+
* @param c - the context
|
|
447
|
+
* @param eles - the elements
|
|
448
|
+
* @returns the collection
|
|
449
|
+
*/
|
|
450
|
+
function collect(c: Ctx, eles: readonly (NodeSingular | EdgeSingular)[]): CollectionReturnValue {
|
|
451
|
+
return c.cy.collection(eles as unknown as CollectionArgument);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Throws on a negative edge weight, which Dijkstra and A* cannot take (on an undirected graph Dijkstra would never
|
|
456
|
+
* return). The code matches graphtyBidirectionalDijkstra's error.
|
|
457
|
+
* @param c - the context
|
|
458
|
+
*/
|
|
459
|
+
function nonNegative(c: Ctx): void {
|
|
460
|
+
if (c.s.weights?.some((w) => w < 0) === true) {
|
|
461
|
+
throw Object.assign(new Error(`${c.name}: a weight is negative; use graphtyBellmanFord`), {
|
|
462
|
+
code: "E_BAD_WEIGHT",
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* The index of a required node option.
|
|
469
|
+
* @param c - the context
|
|
470
|
+
* @param sel - the option value
|
|
471
|
+
* @param what - the option name
|
|
472
|
+
* @returns the node index
|
|
473
|
+
*/
|
|
474
|
+
function req(c: Ctx, sel: NodeSelection | undefined, what: string): number {
|
|
475
|
+
const i = indexOf(c.cs, sel, `${c.name}: ${what}`);
|
|
476
|
+
if (i === undefined) {
|
|
477
|
+
throw new Error(`${c.name}: the ${what} option is required`);
|
|
478
|
+
}
|
|
479
|
+
return i;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* The index of a node or edge reference, or undefined when it is not in the snapshot.
|
|
484
|
+
* @param c - the context
|
|
485
|
+
* @param ref - an element or a selector
|
|
486
|
+
* @param edges - look among the edges instead of the nodes
|
|
487
|
+
* @returns the index
|
|
488
|
+
*/
|
|
489
|
+
function lookup(c: Ctx, ref: ElementRef, edges = false): number | undefined {
|
|
490
|
+
const list = (edges ? c.cs.edges : c.cs.nodes) as unknown as Collection;
|
|
491
|
+
// a selector goes through Cytoscape's own matching (a collection has no find() that takes one)
|
|
492
|
+
const matches = typeof ref === "string" ? list.filter(ref) : (ref as Collection);
|
|
493
|
+
const ele = matches[0];
|
|
494
|
+
if (ele === undefined) {
|
|
495
|
+
return undefined;
|
|
496
|
+
}
|
|
497
|
+
if (!edges) {
|
|
498
|
+
const i = c.s.ids.indexOf(ele.id());
|
|
499
|
+
return i === INVALID_INDEX ? undefined : i;
|
|
500
|
+
}
|
|
501
|
+
// ponytail: linear scan per edge lookup; an id -> index map if edge accessors show up in a profile.
|
|
502
|
+
const i = c.cs.edges.toArray().indexOf(ele as EdgeSingular);
|
|
503
|
+
return i < 0 ? undefined : i;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* An accessor from element to value.
|
|
508
|
+
* @param c - the context
|
|
509
|
+
* @param values - one value per node (or edge) index
|
|
510
|
+
* @param edges - the values are per edge
|
|
511
|
+
* @returns the accessor
|
|
512
|
+
*/
|
|
513
|
+
function accessor(c: Ctx, values: ArrayLike<number>, edges = false): (ref: ElementRef) => number | undefined {
|
|
514
|
+
return (ref) => {
|
|
515
|
+
const i = lookup(c, ref, edges);
|
|
516
|
+
return i === undefined ? undefined : values[i];
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* The nodes at some indices, as a collection in that order.
|
|
522
|
+
* @param c - the context
|
|
523
|
+
* @param idx - node indices
|
|
524
|
+
* @returns the collection
|
|
525
|
+
*/
|
|
526
|
+
function nodesAt(c: Ctx, idx: ArrayLike<number>): NodeCollection {
|
|
527
|
+
return collect(
|
|
528
|
+
c,
|
|
529
|
+
Array.from(idx, (i) => c.cs.nodes[i]),
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* The edges at some indices, as a collection in that order.
|
|
535
|
+
* @param c - the context
|
|
536
|
+
* @param idx - edge indices
|
|
537
|
+
* @returns the collection
|
|
538
|
+
*/
|
|
539
|
+
function edgesAt(c: Ctx, idx: ArrayLike<number>): EdgeCollection {
|
|
540
|
+
return collect(
|
|
541
|
+
c,
|
|
542
|
+
Array.from(idx, (i) => c.cs.edges[i]),
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* A path as Cytoscape's path algorithms return it: node, edge, node, ...
|
|
548
|
+
* @param c - the context
|
|
549
|
+
* @param nodes - node indices along the path
|
|
550
|
+
* @param edges - edge indices along the path, one fewer
|
|
551
|
+
* @returns the collection
|
|
552
|
+
*/
|
|
553
|
+
function pathOf(c: Ctx, nodes: ArrayLike<number>, edges: ArrayLike<number>): CollectionReturnValue {
|
|
554
|
+
const out: (NodeSingular | EdgeSingular)[] = [];
|
|
555
|
+
for (let k = 0; k < nodes.length; k++) {
|
|
556
|
+
if (k > 0) {
|
|
557
|
+
out.push(c.cs.edges[edges[k - 1]]);
|
|
558
|
+
}
|
|
559
|
+
out.push(c.cs.nodes[nodes[k]]);
|
|
560
|
+
}
|
|
561
|
+
return collect(c, out);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* The node indices of each selection in a list.
|
|
566
|
+
* @param c - the context
|
|
567
|
+
* @param pairs - node pairs
|
|
568
|
+
* @param what - the option name
|
|
569
|
+
* @returns the sources and targets
|
|
570
|
+
*/
|
|
571
|
+
function pairsOf(
|
|
572
|
+
c: Ctx,
|
|
573
|
+
pairs: readonly NodePair[] | undefined,
|
|
574
|
+
what: string,
|
|
575
|
+
): { sources: number[]; targets: number[] } {
|
|
576
|
+
if (pairs === undefined) {
|
|
577
|
+
throw new Error(`${c.name}: the ${what} option is required`);
|
|
578
|
+
}
|
|
579
|
+
return { sources: pairs.map((p) => req(c, p[0], what)), targets: pairs.map((p) => req(c, p[1], what)) };
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* A node mask of a selection.
|
|
584
|
+
* @param c - the context
|
|
585
|
+
* @param sel - a selection, or undefined
|
|
586
|
+
* @returns the mask, or undefined
|
|
587
|
+
*/
|
|
588
|
+
function maskOf(c: Ctx, sel: NodeSelection | undefined): U32 | undefined {
|
|
589
|
+
if (sel === undefined) {
|
|
590
|
+
return undefined;
|
|
591
|
+
}
|
|
592
|
+
const m = makeMask(c.s.nodeCount);
|
|
593
|
+
for (const i of indicesOf(c.cs, sel)) {
|
|
594
|
+
maskSet(m, i, true);
|
|
595
|
+
}
|
|
596
|
+
return m;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// ---------------------------------------------------------------------------------------------------------------
|
|
600
|
+
// Result builders
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* A score result: `score(ele)` plus extras; writes `field`.
|
|
604
|
+
* @param c - the context
|
|
605
|
+
* @param values - one value per node (or edge) index
|
|
606
|
+
* @param extra - other fields of the result
|
|
607
|
+
* @param edges - the values are per edge
|
|
608
|
+
* @returns the result
|
|
609
|
+
*/
|
|
610
|
+
function scored<T extends object>(c: Ctx, values: ArrayLike<number>, extra: T, edges = false): ScoreResult & T {
|
|
611
|
+
if (c.field !== undefined) {
|
|
612
|
+
writeData(edges ? c.cs.edges : c.cs.nodes, values, c.field);
|
|
613
|
+
}
|
|
614
|
+
return { score: accessor(c, values, edges), ...extra };
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* A partition: one node collection per label, with `cluster(node)` and extras; writes `field`.
|
|
619
|
+
* @param c - the context
|
|
620
|
+
* @param labels - the label of every node index, 0..count-1 (INVALID_INDEX: in no cluster)
|
|
621
|
+
* @param extra - other fields of the result
|
|
622
|
+
* @returns the partition
|
|
623
|
+
*/
|
|
624
|
+
function partition<T extends object>(c: Ctx, labels: ArrayLike<number>, extra: T): Partition<T> {
|
|
625
|
+
const groups: number[][] = [];
|
|
626
|
+
for (let i = 0; i < labels.length; i++) {
|
|
627
|
+
const l = labels[i];
|
|
628
|
+
if (l !== INVALID_INDEX) {
|
|
629
|
+
const group = groups[l] ?? [];
|
|
630
|
+
group.push(i);
|
|
631
|
+
groups[l] = group;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
if (c.field !== undefined) {
|
|
635
|
+
writeData(c.cs.nodes, labels, c.field);
|
|
636
|
+
}
|
|
637
|
+
const out = Array.from(groups, (g) => nodesAt(c, g ?? []));
|
|
638
|
+
return Object.assign(out, extra, {
|
|
639
|
+
cluster: (ref: ElementRef) => {
|
|
640
|
+
const i = lookup(c, ref);
|
|
641
|
+
return i === undefined || labels[i] === INVALID_INDEX ? undefined : labels[i];
|
|
642
|
+
},
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* A walk result (BFS, DFS).
|
|
648
|
+
* @param c - the context
|
|
649
|
+
* @param r - the walk
|
|
650
|
+
* @param r.order - visited node indices in visit order
|
|
651
|
+
* @param r.parent - parent node index per node
|
|
652
|
+
* @param r.depth - depth per node
|
|
653
|
+
* @param r.visitedCount - number of visited nodes
|
|
654
|
+
* @param target - the target index, if any
|
|
655
|
+
* @returns the search result
|
|
656
|
+
*/
|
|
657
|
+
function search(
|
|
658
|
+
c: Ctx,
|
|
659
|
+
r: { order: Uint32Array; parent: Uint32Array; depth: Uint32Array; visitedCount: number },
|
|
660
|
+
target: number | undefined,
|
|
661
|
+
): SearchResult {
|
|
662
|
+
const out: (NodeSingular | EdgeSingular)[] = [];
|
|
663
|
+
for (let k = 0; k < r.visitedCount; k++) {
|
|
664
|
+
const v = r.order[k];
|
|
665
|
+
const p = r.parent[v];
|
|
666
|
+
if (p !== INVALID_INDEX) {
|
|
667
|
+
// The walk records the parent node, not the arc, so the tree edge is looked up: the first edge of the
|
|
668
|
+
// collection joining the two (CPU BFS/DFS results have no `predArc`, unlike Dijkstra's).
|
|
669
|
+
const from = c.cs.nodes[p];
|
|
670
|
+
const to = c.cs.nodes[v];
|
|
671
|
+
const joining = c.s.directed ? from.edgesTo(to) : from.edgesWith(to);
|
|
672
|
+
const e = joining.intersection(c.cs.edges)[0];
|
|
673
|
+
if (e !== undefined) {
|
|
674
|
+
out.push(e);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
out.push(c.cs.nodes[v]);
|
|
678
|
+
}
|
|
679
|
+
const reached = target !== undefined && r.depth[target] !== INVALID_INDEX;
|
|
680
|
+
if (c.field !== undefined) {
|
|
681
|
+
writeData(
|
|
682
|
+
c.cs.nodes,
|
|
683
|
+
Array.from(r.depth, (d) => (d === INVALID_INDEX ? Number.NaN : d)),
|
|
684
|
+
c.field,
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
return {
|
|
688
|
+
path: collect(c, out),
|
|
689
|
+
found: collect(c, reached ? [c.cs.nodes[target]] : []),
|
|
690
|
+
depth: (ref) => {
|
|
691
|
+
const i = lookup(c, ref);
|
|
692
|
+
return i === undefined || r.depth[i] === INVALID_INDEX ? undefined : r.depth[i];
|
|
693
|
+
},
|
|
694
|
+
parent: (ref) => {
|
|
695
|
+
const i = lookup(c, ref);
|
|
696
|
+
return i === undefined || r.parent[i] === INVALID_INDEX ? undefined : c.cs.nodes[r.parent[i]];
|
|
697
|
+
},
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* A single-source shortest-path result.
|
|
703
|
+
* @param c - the context
|
|
704
|
+
* @param r - the result
|
|
705
|
+
* @param r.dist - distance per node
|
|
706
|
+
* @param r.pathTo - node path to a target
|
|
707
|
+
* @param r.pathEdges - edge path to a target
|
|
708
|
+
* @returns the accessors
|
|
709
|
+
*/
|
|
710
|
+
function paths(
|
|
711
|
+
c: Ctx,
|
|
712
|
+
r: { dist: ArrayLike<number>; pathTo(t: number): Uint32Array; pathEdges(t: number): Uint32Array },
|
|
713
|
+
): PathsResult {
|
|
714
|
+
if (c.field !== undefined) {
|
|
715
|
+
writeData(c.cs.nodes, r.dist, c.field);
|
|
716
|
+
}
|
|
717
|
+
return {
|
|
718
|
+
distanceTo: (ref) => {
|
|
719
|
+
const i = lookup(c, ref);
|
|
720
|
+
return i === undefined ? Infinity : r.dist[i];
|
|
721
|
+
},
|
|
722
|
+
pathTo: (ref) => {
|
|
723
|
+
const i = lookup(c, ref);
|
|
724
|
+
return i === undefined ? c.cy.collection() : pathOf(c, r.pathTo(i), r.pathEdges(i));
|
|
725
|
+
},
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* A cut result.
|
|
731
|
+
* @param c - the context
|
|
732
|
+
* @param side - the first side as a node mask
|
|
733
|
+
* @param cutEdges - the cut edge indices
|
|
734
|
+
* @param value - the cut's weight
|
|
735
|
+
* @returns the cut
|
|
736
|
+
*/
|
|
737
|
+
function cutOf(c: Ctx, side: U32, cutEdges: ArrayLike<number>, value: number): CutResult {
|
|
738
|
+
const first: number[] = [];
|
|
739
|
+
const second: number[] = [];
|
|
740
|
+
for (let i = 0; i < c.s.nodeCount; i++) {
|
|
741
|
+
(maskTest(side, i) ? first : second).push(i);
|
|
742
|
+
}
|
|
743
|
+
if (c.field !== undefined) {
|
|
744
|
+
writeData(
|
|
745
|
+
c.cs.nodes,
|
|
746
|
+
Array.from({ length: c.s.nodeCount }, (_, i) => (maskTest(side, i) ? 0 : 1)),
|
|
747
|
+
c.field,
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
return { value, cut: edgesAt(c, cutEdges), partitionFirst: nodesAt(c, first), partitionSecond: nodesAt(c, second) };
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* A minimum cut result.
|
|
755
|
+
* @param c - the context
|
|
756
|
+
* @param r - the cut
|
|
757
|
+
* @returns the cut
|
|
758
|
+
*/
|
|
759
|
+
function minCut(c: Ctx, r: MinCutResult): CutResult {
|
|
760
|
+
return cutOf(c, r.side, r.cutEdges, r.cutValue);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Predicted links with their nodes.
|
|
765
|
+
* @param c - the context
|
|
766
|
+
* @param r - the prediction
|
|
767
|
+
* @returns one entry per predicted pair, best first
|
|
768
|
+
*/
|
|
769
|
+
function links(c: Ctx, r: LinkPredictionResult): PredictedLink[] {
|
|
770
|
+
return Array.from(r.scores, (score, k) => ({
|
|
771
|
+
source: c.cs.nodes[r.sources[k]],
|
|
772
|
+
target: c.cs.nodes[r.targets[k]],
|
|
773
|
+
score,
|
|
774
|
+
}));
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* A node-to-node mapping accessor.
|
|
779
|
+
* @param c - the context
|
|
780
|
+
* @param other - the image collection's snapshot
|
|
781
|
+
* @param mapping - image index per node index
|
|
782
|
+
* @returns the accessor
|
|
783
|
+
*/
|
|
784
|
+
function mapper(
|
|
785
|
+
c: Ctx,
|
|
786
|
+
other: CytoscapeSnapshot,
|
|
787
|
+
mapping: Uint32Array,
|
|
788
|
+
): (node: ElementRef) => NodeSingular | undefined {
|
|
789
|
+
return (ref) => {
|
|
790
|
+
const i = lookup(c, ref);
|
|
791
|
+
return i === undefined ? undefined : other.nodes[mapping[i]];
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* The two-snapshot isomorphism options, with element callbacks turned into index callbacks.
|
|
797
|
+
* @param c - the context
|
|
798
|
+
* @param other - the second graph
|
|
799
|
+
* @param o - the options
|
|
800
|
+
* @returns the options for @graphty/algorithms
|
|
801
|
+
*/
|
|
802
|
+
function isoOptions(c: Ctx, other: CytoscapeSnapshot, o: IsomorphismOptions): Record<string, unknown> {
|
|
803
|
+
const { nodeMatch, edgeMatch } = o;
|
|
804
|
+
return {
|
|
805
|
+
...c.rest,
|
|
806
|
+
nodeMatch: nodeMatch && ((a: number, b: number) => nodeMatch(c.cs.nodes[a], other.nodes[b])),
|
|
807
|
+
edgeMatch: edgeMatch && ((a: number, b: number) => edgeMatch(c.cs.edges[a], other.edges[b])),
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* The snapshot of the second graph of an isomorphism test, read like the first.
|
|
813
|
+
* @param c - the context
|
|
814
|
+
* @param o - the options naming it
|
|
815
|
+
* @returns its snapshot
|
|
816
|
+
*/
|
|
817
|
+
function otherOf(c: Ctx, o: IsomorphismOptions): CytoscapeSnapshot {
|
|
818
|
+
const other = o.other as unknown;
|
|
819
|
+
// required by the type, checked for callers without types
|
|
820
|
+
if (other === undefined) {
|
|
821
|
+
throw new Error(`${c.name}: the other option (the collection to compare with) is required`);
|
|
822
|
+
}
|
|
823
|
+
// a selector (over the whole core) or an array of elements names the other graph too
|
|
824
|
+
let eles = other;
|
|
825
|
+
if (typeof other === "string") {
|
|
826
|
+
eles = c.cy.$(other);
|
|
827
|
+
} else if (Array.isArray(other)) {
|
|
828
|
+
// Cytoscape takes an array of elements here; its typings list only definitions
|
|
829
|
+
eles = c.cy.collection(other);
|
|
830
|
+
}
|
|
831
|
+
if (typeof (eles as Partial<Collection> | null)?.nodes !== "function") {
|
|
832
|
+
throw new TypeError(
|
|
833
|
+
`${c.name}: the other option must be a Cytoscape collection, a selector or an array of elements`,
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
return toSnapshot(eles as Collection, { directed: c.s.directed });
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* A per-node vector from a function of the node or a selection (1 for selected nodes, 0 for the rest).
|
|
841
|
+
* @param c - the context
|
|
842
|
+
* @param v - the function or selection
|
|
843
|
+
* @param what - the option name
|
|
844
|
+
* @returns the vector
|
|
845
|
+
*/
|
|
846
|
+
function vectorOf(c: Ctx, v: NodeSelection | ((node: NodeSingular) => number) | undefined, what: string): F64 {
|
|
847
|
+
if (v === undefined) {
|
|
848
|
+
throw new Error(`${c.name}: the ${what} option is required`);
|
|
849
|
+
}
|
|
850
|
+
if (typeof v === "function") {
|
|
851
|
+
return Float64Array.from(c.cs.nodes.toArray(), (n) => v(n));
|
|
852
|
+
}
|
|
853
|
+
const out = new Float64Array(c.s.nodeCount);
|
|
854
|
+
for (const i of indicesOf(c.cs, v)) {
|
|
855
|
+
out[i] = 1;
|
|
856
|
+
}
|
|
857
|
+
return out;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* The teleport vector of a personalized PageRank. One that selects no node would run plain PageRank without a word,
|
|
862
|
+
* so it throws, as a required node option that matches nothing does.
|
|
863
|
+
* @param c - the context
|
|
864
|
+
* @param v - the selection or the weight function
|
|
865
|
+
* @returns the vector
|
|
866
|
+
*/
|
|
867
|
+
function teleportOf(c: Ctx, v: NodeSelection | ((node: NodeSingular) => number) | undefined): F64 {
|
|
868
|
+
const p = vectorOf(c, v, "personalization");
|
|
869
|
+
if (!p.some((x) => x > 0)) {
|
|
870
|
+
throw new Error(
|
|
871
|
+
`${c.name}: personalization ${typeof v === "function" ? "gives no node a positive weight" : "matches no node"}`,
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
return p;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Labels per node from a list of clusters or a node data field.
|
|
879
|
+
* @param c - the context
|
|
880
|
+
* @param clusters - the clusters, or the data field holding each node's cluster
|
|
881
|
+
* @param what - the option name, for the error
|
|
882
|
+
* @returns label per node index (INVALID_INDEX: none)
|
|
883
|
+
*/
|
|
884
|
+
function labelsOf(c: Ctx, clusters: string | readonly NodeSelection[] | undefined, what: string): U32 {
|
|
885
|
+
if (clusters === undefined) {
|
|
886
|
+
throw new Error(`${c.name}: the ${what} option is required`);
|
|
887
|
+
}
|
|
888
|
+
const labels = new Uint32Array(c.s.nodeCount).fill(INVALID_INDEX);
|
|
889
|
+
if (typeof clusters === "string") {
|
|
890
|
+
const ids = new Map<unknown, number>();
|
|
891
|
+
c.cs.nodes.forEach((n, i) => {
|
|
892
|
+
const v: unknown = n.data(clusters);
|
|
893
|
+
if (v !== undefined) {
|
|
894
|
+
labels[i] = ids.get(v) ?? ids.set(v, ids.size).size - 1;
|
|
895
|
+
}
|
|
896
|
+
});
|
|
897
|
+
} else {
|
|
898
|
+
clusters.forEach((sel, k) => {
|
|
899
|
+
for (const i of indicesOf(c.cs, sel)) {
|
|
900
|
+
labels[i] = k;
|
|
901
|
+
}
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
// a misspelled field or selector would otherwise give a plausible result (modularity 0, an unseeded partition)
|
|
905
|
+
if (labels.every((l) => l === INVALID_INDEX)) {
|
|
906
|
+
throw new Error(
|
|
907
|
+
typeof clusters === "string"
|
|
908
|
+
? `${c.name}: ${what} names the data field "${clusters}", which no node has`
|
|
909
|
+
: `${c.name}: ${what} matches no node`,
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
return labels;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/** The all-pairs result. */
|
|
916
|
+
interface ApspResult {
|
|
917
|
+
distance(from: ElementRef, to: ElementRef): number;
|
|
918
|
+
/** The path; throws when the run was made with `paths: false`. */
|
|
919
|
+
path(from: ElementRef, to: ElementRef): CollectionReturnValue;
|
|
920
|
+
readonly hasNegativeWeightCycle: boolean;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* The all-pairs accessors.
|
|
925
|
+
* @param c - the context
|
|
926
|
+
* @param r - the distance matrix, with the path walkers when it was run with `paths: true`
|
|
927
|
+
* @returns the accessors
|
|
928
|
+
*/
|
|
929
|
+
function apsp(
|
|
930
|
+
c: Ctx,
|
|
931
|
+
r: Out<"allPairsShortestPath"> & {
|
|
932
|
+
pathTo?: (i: number, j: number) => Uint32Array;
|
|
933
|
+
pathEdges?: (i: number, j: number) => Uint32Array;
|
|
934
|
+
},
|
|
935
|
+
): ApspResult {
|
|
936
|
+
const pair = (a: ElementRef, b: ElementRef): [number, number] | undefined => {
|
|
937
|
+
const i = lookup(c, a);
|
|
938
|
+
const j = lookup(c, b);
|
|
939
|
+
return i === undefined || j === undefined ? undefined : [i, j];
|
|
940
|
+
};
|
|
941
|
+
return {
|
|
942
|
+
hasNegativeWeightCycle: r.hasNegativeCycle,
|
|
943
|
+
distance: (a, b) => {
|
|
944
|
+
const p = pair(a, b);
|
|
945
|
+
return p === undefined ? Infinity : r.dist[p[0] * r.n + p[1]];
|
|
946
|
+
},
|
|
947
|
+
path: (a, b) => {
|
|
948
|
+
const { pathTo, pathEdges } = r;
|
|
949
|
+
if (pathTo === undefined || pathEdges === undefined) {
|
|
950
|
+
// the same text the CPU result throws, so the message does not depend on the backend
|
|
951
|
+
throw new Error("allPairsShortestPath: pass paths: true to walk shortest paths");
|
|
952
|
+
}
|
|
953
|
+
const p = pair(a, b);
|
|
954
|
+
return p === undefined ? c.cy.collection() : pathOf(c, pathTo(p[0], p[1]), pathEdges(p[0], p[1]));
|
|
955
|
+
},
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
/** A PageRank result. */
|
|
960
|
+
type RankResult = ScoreResult & { rank(node: ElementRef): number | undefined; iterations: number; converged: boolean };
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* A PageRank result from scores.
|
|
964
|
+
* @param c - the context
|
|
965
|
+
* @param r - the run
|
|
966
|
+
* @returns the result
|
|
967
|
+
*/
|
|
968
|
+
function ranked(c: Ctx, r: Out<"pageRank">): RankResult {
|
|
969
|
+
return scored(c, r.scores, { rank: accessor(c, r.scores), iterations: r.iterations, converged: r.converged });
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* A label-propagation partition. The GPU result has no `iterations` / `converged` (the WebGPU label propagation
|
|
974
|
+
* does not report them), so both are undefined when the GPU ran.
|
|
975
|
+
* @param c - the context
|
|
976
|
+
* @param r - the run
|
|
977
|
+
* @returns the partition
|
|
978
|
+
*/
|
|
979
|
+
function propagated(
|
|
980
|
+
c: Ctx,
|
|
981
|
+
r: Out<"labelPropagation"> & { iterations?: number; converged?: boolean },
|
|
982
|
+
): Partition<LpaReport> {
|
|
983
|
+
return partition(c, r.labels, { iterations: r.iterations, converged: r.converged });
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/** What label propagation reports beside the partition; undefined when the GPU ran. */
|
|
987
|
+
interface LpaReport {
|
|
988
|
+
iterations: number | undefined;
|
|
989
|
+
converged: boolean | undefined;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/** A result that is plain from a plain method and a promise from an `...Async` method. */
|
|
993
|
+
type MaybeAsync<T> = T | Promise<T>;
|
|
994
|
+
|
|
995
|
+
// ---------------------------------------------------------------------------------------------------------------
|
|
996
|
+
// Option types
|
|
997
|
+
|
|
998
|
+
interface Rooted extends AlgorithmOptions {
|
|
999
|
+
/** The start node: a selector or a collection (its first node). */
|
|
1000
|
+
readonly root: NodeSelection;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
interface PointToPoint extends Rooted {
|
|
1004
|
+
/** The node to reach. */
|
|
1005
|
+
readonly goal: NodeSelection;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
interface WalkOptions extends Rooted {
|
|
1009
|
+
/** Stop when this node is reached; it is then `found`. */
|
|
1010
|
+
readonly goal?: NodeSelection;
|
|
1011
|
+
/** Stop expanding at this many hops from the root; unbounded when absent. */
|
|
1012
|
+
readonly maxDepth?: number;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
interface FlowOptions extends AlgorithmOptions, Omit<MaxFlowOptions, "weights"> {
|
|
1016
|
+
/** The node the flow leaves. */
|
|
1017
|
+
readonly source: NodeSelection;
|
|
1018
|
+
/** The node the flow arrives at. */
|
|
1019
|
+
readonly sink: NodeSelection;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
interface IsomorphismOptions extends AlgorithmOptions {
|
|
1023
|
+
/**
|
|
1024
|
+
* The graph to compare with, read with the same `directed`: a collection, an array of elements, or a selector
|
|
1025
|
+
* over the whole core (for example ".second", matching that graph's nodes and edges).
|
|
1026
|
+
*/
|
|
1027
|
+
readonly other: Collection | readonly (NodeSingular | EdgeSingular)[] | string;
|
|
1028
|
+
/** Two nodes may be paired only when this returns true; by default any two may. */
|
|
1029
|
+
readonly nodeMatch?: (a: NodeSingular, b: NodeSingular) => boolean;
|
|
1030
|
+
/** Two edges may be paired only when this returns true; by default any two may. */
|
|
1031
|
+
readonly edgeMatch?: (a: EdgeSingular, b: EdgeSingular) => boolean;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
interface MatchingOptions extends AlgorithmOptions {
|
|
1035
|
+
/** One side of the bipartite graph; inferred when absent. */
|
|
1036
|
+
readonly left?: NodeSelection;
|
|
1037
|
+
/** The other side; inferred when absent. */
|
|
1038
|
+
readonly right?: NodeSelection;
|
|
1039
|
+
/** With `directed`: "both" (default) ignores direction; "out" joins a left node only to its out-neighbors. */
|
|
1040
|
+
readonly arcs?: "both" | "out";
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
/** A cap on path length. */
|
|
1044
|
+
interface Cutoff {
|
|
1045
|
+
/** Stop relaxing beyond this distance; a node farther away reads as unreachable. Unbounded when absent. */
|
|
1046
|
+
readonly cutoff?: number;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
/** Sampled centrality. */
|
|
1050
|
+
interface Sampled {
|
|
1051
|
+
/** The nodes to run from, a selector or a collection; default every node. */
|
|
1052
|
+
readonly sources?: NodeSelection;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
/** One node pair. */
|
|
1056
|
+
interface PairOptions {
|
|
1057
|
+
/** One node of the pair. */
|
|
1058
|
+
readonly source: NodeSelection;
|
|
1059
|
+
/** The other node of the pair. */
|
|
1060
|
+
readonly target: NodeSelection;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
/** Several node pairs. */
|
|
1064
|
+
interface PairsOptions {
|
|
1065
|
+
/** The node pairs to score, each `[a, b]` of selectors or collections. */
|
|
1066
|
+
readonly pairs: readonly NodePair[];
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
/** The nodes link prediction may propose. */
|
|
1070
|
+
interface Candidates {
|
|
1071
|
+
/** The nodes to consider linking to `root`; default every node. */
|
|
1072
|
+
readonly candidates?: NodeSelection;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
/**
|
|
1076
|
+
* The `topK` of a candidates method with the meaning it has on the prediction methods: a positive count keeps that
|
|
1077
|
+
* many, 0 or less keeps every candidate (@graphty/algorithms slices with it, so 0 would keep none and -1 drop one).
|
|
1078
|
+
* @param o - the options
|
|
1079
|
+
* @param o.topK - the caller's value
|
|
1080
|
+
* @returns the value for @graphty/algorithms
|
|
1081
|
+
*/
|
|
1082
|
+
function topK(o: { readonly topK?: number }): number | undefined {
|
|
1083
|
+
return o.topK !== undefined && o.topK <= 0 ? Infinity : o.topK;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
interface MatchingResult {
|
|
1087
|
+
readonly size: number;
|
|
1088
|
+
/** The node matched with this one, either side. */
|
|
1089
|
+
mate(node: ElementRef): NodeSingular | undefined;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
type WeightedFlag<T> = AlgorithmOptions & Omit<T, "weighted" | "weights">;
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* A matching result.
|
|
1096
|
+
* @param c - the context
|
|
1097
|
+
* @param r - the matching
|
|
1098
|
+
* @param r.matching - partner per node index
|
|
1099
|
+
* @param r.size - matched pairs
|
|
1100
|
+
* @returns mate accessor and size
|
|
1101
|
+
*/
|
|
1102
|
+
function matched(c: Ctx, r: { matching: Uint32Array; size: number }): MatchingResult {
|
|
1103
|
+
const mate = new Uint32Array(c.s.nodeCount).fill(INVALID_INDEX);
|
|
1104
|
+
r.matching.forEach((m, i) => {
|
|
1105
|
+
if (m !== INVALID_INDEX) {
|
|
1106
|
+
mate[i] = m;
|
|
1107
|
+
mate[m] = i;
|
|
1108
|
+
}
|
|
1109
|
+
});
|
|
1110
|
+
return {
|
|
1111
|
+
size: r.size,
|
|
1112
|
+
mate: (ref) => {
|
|
1113
|
+
const i = lookup(c, ref);
|
|
1114
|
+
return i === undefined || mate[i] === INVALID_INDEX ? undefined : c.cs.nodes[mate[i]];
|
|
1115
|
+
},
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/**
|
|
1120
|
+
* The best level of a Girvan-Newman run.
|
|
1121
|
+
* @param modularities - modularity per level
|
|
1122
|
+
* @returns the level index with the highest modularity
|
|
1123
|
+
*/
|
|
1124
|
+
function bestLevel(modularities: ArrayLike<number>): number {
|
|
1125
|
+
let best = 0;
|
|
1126
|
+
for (let k = 1; k < modularities.length; k++) {
|
|
1127
|
+
if (modularities[k] > modularities[best]) {
|
|
1128
|
+
best = k;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
return best;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// ---------------------------------------------------------------------------------------------------------------
|
|
1135
|
+
// The algorithms. Key = the @graphty/algorithms name; registered as "graphty" + the key with a capital.
|
|
1136
|
+
|
|
1137
|
+
const IMPLS = {
|
|
1138
|
+
// Traversal and paths
|
|
1139
|
+
breadthFirstSearch: (c: Ctx, o: WalkOptions): MaybeAsync<SearchResult> => {
|
|
1140
|
+
const target = indexOf(c.cs, o.goal, `${c.name}: goal`);
|
|
1141
|
+
return then(c.call("breadthFirstSearch", c.s, req(c, o.root, "root"), { ...c.rest, target }), (r) =>
|
|
1142
|
+
search(c, r, target),
|
|
1143
|
+
);
|
|
1144
|
+
},
|
|
1145
|
+
directionOptimizedBfs: (c: Ctx, o: Rooted & DirectionOptimizedBfsOptions): SearchResult =>
|
|
1146
|
+
search(c, directionOptimizedBfs(c.s, req(c, o.root, "root"), c.rest), undefined),
|
|
1147
|
+
depthFirstSearch: (
|
|
1148
|
+
c: Ctx,
|
|
1149
|
+
o: Omit<WalkOptions, "maxDepth"> & {
|
|
1150
|
+
/** "pre" (default) lists a node when it is first reached, "post" when everything below it is finished. */
|
|
1151
|
+
readonly order?: "pre" | "post";
|
|
1152
|
+
},
|
|
1153
|
+
): SearchResult => {
|
|
1154
|
+
const target = indexOf(c.cs, o.goal, `${c.name}: goal`);
|
|
1155
|
+
// a post-order walk cannot stop at the goal, so it would silently visit every node
|
|
1156
|
+
if (target !== undefined && o.order === "post") {
|
|
1157
|
+
throw new Error(`${c.name}: goal stops only a pre-order walk; leave it out with order "post"`);
|
|
1158
|
+
}
|
|
1159
|
+
return search(c, depthFirstSearch(c.s, req(c, o.root, "root"), { ...c.rest, target }), target);
|
|
1160
|
+
},
|
|
1161
|
+
hasCycle: (c: Ctx, _o: AlgorithmOptions = {}): boolean => hasCycle(c.s),
|
|
1162
|
+
topologicalSort: (c: Ctx, _o: AlgorithmOptions = {}): NodeCollection | null => {
|
|
1163
|
+
const order = topologicalSort(c.s);
|
|
1164
|
+
return order === null ? null : nodesAt(c, order);
|
|
1165
|
+
},
|
|
1166
|
+
dijkstra: (c: Ctx, o: Rooted & Cutoff): PathsResult | Promise<PathsResult> => {
|
|
1167
|
+
nonNegative(c);
|
|
1168
|
+
return then(c.call("sssp", c.s, req(c, o.root, "root"), c.rest), (r) => paths(c, r));
|
|
1169
|
+
},
|
|
1170
|
+
bellmanFord: (c: Ctx, o: Rooted & Cutoff): MaybeAsync<PathsResult & { readonly hasNegativeWeightCycle: boolean }> =>
|
|
1171
|
+
then(c.call("bellmanFord", c.s, req(c, o.root, "root"), c.rest), (r) => ({
|
|
1172
|
+
...paths(c, r),
|
|
1173
|
+
hasNegativeWeightCycle: r.hasNegativeCycle,
|
|
1174
|
+
})),
|
|
1175
|
+
bidirectionalDijkstra: (c: Ctx, o: PointToPoint): PointPathResult => {
|
|
1176
|
+
nonNegative(c);
|
|
1177
|
+
const r = bidirectionalDijkstra(c.s, req(c, o.root, "root"), req(c, o.goal, "goal"), c.rest);
|
|
1178
|
+
return { found: r.path.length > 0, distance: r.distance, path: pathOf(c, r.path, r.edges) };
|
|
1179
|
+
},
|
|
1180
|
+
aStar: (
|
|
1181
|
+
c: Ctx,
|
|
1182
|
+
o: PointToPoint & {
|
|
1183
|
+
/** An estimate of the distance left from a node to `goal`, never more than the real one. Default 0. */
|
|
1184
|
+
readonly heuristic?: (node: NodeSingular) => number;
|
|
1185
|
+
},
|
|
1186
|
+
): PointPathResult => {
|
|
1187
|
+
const h = o.heuristic;
|
|
1188
|
+
nonNegative(c);
|
|
1189
|
+
const r = astar(
|
|
1190
|
+
c.s,
|
|
1191
|
+
req(c, o.root, "root"),
|
|
1192
|
+
req(c, o.goal, "goal"),
|
|
1193
|
+
h === undefined ? () => 0 : (u) => h(c.cs.nodes[u]),
|
|
1194
|
+
c.rest,
|
|
1195
|
+
);
|
|
1196
|
+
return { found: r.path.length > 0, distance: r.distance, path: pathOf(c, r.path, r.edges) };
|
|
1197
|
+
},
|
|
1198
|
+
allPairsShortestPath: (
|
|
1199
|
+
c: Ctx,
|
|
1200
|
+
_o: AlgorithmOptions & Omit<ApspOptions, "weights" | "weighted"> = {},
|
|
1201
|
+
): MaybeAsync<ApspResult> => then(c.call("allPairsShortestPath", c.s, c.rest), (r) => apsp(c, r)),
|
|
1202
|
+
|
|
1203
|
+
// Centrality
|
|
1204
|
+
degreeCentrality: (
|
|
1205
|
+
c: Ctx,
|
|
1206
|
+
_o: AlgorithmOptions & DegreeCentralityOptions = {},
|
|
1207
|
+
): ScoreResult & { degree(node: ElementRef): number | undefined } => {
|
|
1208
|
+
const v = degreeCentrality(c.s, c.rest);
|
|
1209
|
+
return scored(c, v, { degree: accessor(c, v) });
|
|
1210
|
+
},
|
|
1211
|
+
degrees: (
|
|
1212
|
+
c: Ctx,
|
|
1213
|
+
_o: AlgorithmOptions = {},
|
|
1214
|
+
): { indegree(node: ElementRef): number | undefined; outdegree(node: ElementRef): number | undefined } => {
|
|
1215
|
+
const r = degrees(c.s);
|
|
1216
|
+
if (!c.s.directed) {
|
|
1217
|
+
// read both ways, an edge enters and leaves each end: both accessors give the degree (@graphty/algorithms'
|
|
1218
|
+
// degrees counts by source and target whatever the direction)
|
|
1219
|
+
const degree = Float64Array.from(r.inDegree, (d, i) => d + r.outDegree[i]);
|
|
1220
|
+
return { indegree: accessor(c, degree), outdegree: accessor(c, degree) };
|
|
1221
|
+
}
|
|
1222
|
+
return { indegree: accessor(c, r.inDegree), outdegree: accessor(c, r.outDegree) };
|
|
1223
|
+
},
|
|
1224
|
+
pageRank: (
|
|
1225
|
+
c: Ctx,
|
|
1226
|
+
o: WeightedFlag<Omit<PageRankOptions, "initialRanks">> & {
|
|
1227
|
+
/** Starting rank of each node. */
|
|
1228
|
+
readonly initialRanks?: (node: NodeSingular) => number;
|
|
1229
|
+
} = {},
|
|
1230
|
+
): MaybeAsync<RankResult> => {
|
|
1231
|
+
const initialRanks = o.initialRanks && vectorOf(c, o.initialRanks, "initialRanks");
|
|
1232
|
+
return then(c.call("pageRank", c.s, { ...c.rest, initialRanks, weighted: c.weighted }), (r) => ranked(c, r));
|
|
1233
|
+
},
|
|
1234
|
+
personalizedPageRank: (
|
|
1235
|
+
c: Ctx,
|
|
1236
|
+
o: WeightedFlag<Omit<PageRankOptions, "initialRanks">> & {
|
|
1237
|
+
/** The teleport set: a selection (uniform over it) or a weight per node. */
|
|
1238
|
+
readonly personalization: NodeSelection | ((node: NodeSingular) => number);
|
|
1239
|
+
/** Starting rank of each node. */
|
|
1240
|
+
readonly initialRanks?: (node: NodeSingular) => number;
|
|
1241
|
+
},
|
|
1242
|
+
): MaybeAsync<RankResult> => {
|
|
1243
|
+
const p = teleportOf(c, o.personalization);
|
|
1244
|
+
const initialRanks = o.initialRanks && vectorOf(c, o.initialRanks, "initialRanks");
|
|
1245
|
+
return then(c.call("personalizedPageRank", c.s, p, { ...c.rest, initialRanks, weighted: c.weighted }), (r) =>
|
|
1246
|
+
ranked(c, r),
|
|
1247
|
+
);
|
|
1248
|
+
},
|
|
1249
|
+
deltaPageRank: (
|
|
1250
|
+
c: Ctx,
|
|
1251
|
+
o: WeightedFlag<Omit<DeltaPageRankComputeOptions, "personalization">> & {
|
|
1252
|
+
/**
|
|
1253
|
+
* Process the largest pending delta first (`PriorityDeltaPageRank`, which ignores `personalization`); its
|
|
1254
|
+
* ranks match `graphtyPageRank({ directed: true })`. Without it the ranks can differ from PageRank's (by up
|
|
1255
|
+
* to 0.07 on the karate club), the run usually goes on until `maxIterations`, and a `maxIterations` in the
|
|
1256
|
+
* thousands overflows and throws.
|
|
1257
|
+
*/
|
|
1258
|
+
readonly priority?: boolean;
|
|
1259
|
+
/** The teleport set: a selection (uniform over it) or a weight per node. */
|
|
1260
|
+
readonly personalization?: NodeSelection | ((node: NodeSingular) => number);
|
|
1261
|
+
} = {},
|
|
1262
|
+
): ScoreResult & { rank(node: ElementRef): number | undefined } => {
|
|
1263
|
+
// The engines keep state for `update()` over the same frozen snapshot; a changed Cytoscape graph is a new
|
|
1264
|
+
// snapshot, so only the one-shot computation is offered.
|
|
1265
|
+
const personalization = o.personalization === undefined ? undefined : teleportOf(c, o.personalization);
|
|
1266
|
+
// PriorityDeltaPageRank's maxIterations counts processed nodes, so the pinned 100 rounds would stop it long
|
|
1267
|
+
// before it converges on any but a tiny graph: without the caller's own budget it runs until no delta is left
|
|
1268
|
+
const budget = o.priority === true && o.maxIterations === undefined ? { maxIterations: Infinity } : {};
|
|
1269
|
+
const opts = { ...c.rest, ...budget, personalization, weighted: c.weighted };
|
|
1270
|
+
const v =
|
|
1271
|
+
o.priority === true
|
|
1272
|
+
? new PriorityDeltaPageRank(c.s).computeWithPriority(opts)
|
|
1273
|
+
: new DeltaPageRank(c.s).compute(opts);
|
|
1274
|
+
// ponytail: @graphty/algorithms' DeltaPageRank overflows to NaN at a large maxIterations; refuse it here
|
|
1275
|
+
// until that engine converges (then this check never fires)
|
|
1276
|
+
if (v.some((x) => !Number.isFinite(x))) {
|
|
1277
|
+
throw new RangeError(
|
|
1278
|
+
`${c.name}: the ranks overflowed (maxIterations ${String(c.rest.maxIterations)} is too high for this graph); ` +
|
|
1279
|
+
"lower maxIterations, or pass priority: true",
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
return scored(c, v, { rank: accessor(c, v) });
|
|
1283
|
+
},
|
|
1284
|
+
eigenvectorCentrality: (
|
|
1285
|
+
c: Ctx,
|
|
1286
|
+
o: AlgorithmOptions &
|
|
1287
|
+
Omit<EigenvectorOptions, "startVector"> & {
|
|
1288
|
+
/** Starting value of each node; default uniform. */
|
|
1289
|
+
readonly startVector?: (node: NodeSingular) => number;
|
|
1290
|
+
} = {},
|
|
1291
|
+
): MaybeAsync<ScoreResult & { iterations: number; converged: boolean }> => {
|
|
1292
|
+
const startVector = o.startVector && vectorOf(c, o.startVector, "startVector");
|
|
1293
|
+
return then(c.call("eigenvectorCentrality", c.s, { ...c.rest, startVector }), (r) =>
|
|
1294
|
+
scored(c, r.scores, { iterations: r.iterations, converged: r.converged }),
|
|
1295
|
+
);
|
|
1296
|
+
},
|
|
1297
|
+
katzCentrality: (
|
|
1298
|
+
c: Ctx,
|
|
1299
|
+
_o: WeightedFlag<KatzOptions> = {},
|
|
1300
|
+
): MaybeAsync<ScoreResult & { iterations: number; converged: boolean }> =>
|
|
1301
|
+
then(c.call("katzCentrality", c.s, { ...c.rest, weighted: c.weighted }), (r) =>
|
|
1302
|
+
scored(c, r.scores, { iterations: r.iterations, converged: r.converged }),
|
|
1303
|
+
),
|
|
1304
|
+
hits: (
|
|
1305
|
+
c: Ctx,
|
|
1306
|
+
_o: WeightedFlag<HitsOptions> = {},
|
|
1307
|
+
): MaybeAsync<
|
|
1308
|
+
ScoreResult & {
|
|
1309
|
+
hub(node: ElementRef): number | undefined;
|
|
1310
|
+
authority(node: ElementRef): number | undefined;
|
|
1311
|
+
iterations: number;
|
|
1312
|
+
converged: boolean;
|
|
1313
|
+
}
|
|
1314
|
+
> =>
|
|
1315
|
+
then(c.call("hits", c.s, { ...c.rest, weighted: c.weighted }), (r) =>
|
|
1316
|
+
scored(c, r.authorities, {
|
|
1317
|
+
hub: accessor(c, r.hubs),
|
|
1318
|
+
authority: accessor(c, r.authorities),
|
|
1319
|
+
iterations: r.iterations,
|
|
1320
|
+
converged: r.converged,
|
|
1321
|
+
}),
|
|
1322
|
+
),
|
|
1323
|
+
closenessCentrality: (
|
|
1324
|
+
c: Ctx,
|
|
1325
|
+
o: WeightedFlag<Omit<ClosenessOptions, "sources">> & Sampled = {},
|
|
1326
|
+
): MaybeAsync<ScoreResult & { closeness(node: ElementRef): number | undefined }> => {
|
|
1327
|
+
const sources = o.sources === undefined ? undefined : indicesOf(c.cs, o.sources);
|
|
1328
|
+
return then(c.call("closenessCentrality", c.s, { ...c.rest, sources, weighted: c.weighted }), (r) =>
|
|
1329
|
+
scored(c, r.scores, { closeness: accessor(c, r.scores) }),
|
|
1330
|
+
);
|
|
1331
|
+
},
|
|
1332
|
+
nodeClosenessCentrality: (
|
|
1333
|
+
c: Ctx,
|
|
1334
|
+
o: Rooted & Omit<ClosenessOptions, "sources" | "k" | "weights" | "weighted">,
|
|
1335
|
+
): number => nodeClosenessCentrality(c.s, req(c, o.root, "root"), { ...c.rest, weighted: c.weighted }),
|
|
1336
|
+
betweennessCentrality: (
|
|
1337
|
+
c: Ctx,
|
|
1338
|
+
o: AlgorithmOptions & Omit<BetweennessOptions, "sources"> & Sampled = {},
|
|
1339
|
+
): MaybeAsync<
|
|
1340
|
+
ScoreResult & {
|
|
1341
|
+
betweenness(node: ElementRef): number | undefined;
|
|
1342
|
+
betweennessNormalized(node: ElementRef): number | undefined;
|
|
1343
|
+
}
|
|
1344
|
+
> => {
|
|
1345
|
+
const sources = o.sources === undefined ? undefined : indicesOf(c.cs, o.sources);
|
|
1346
|
+
return then(c.call("betweennessCentrality", c.s, { ...c.rest, sources }), (r) => {
|
|
1347
|
+
// Cytoscape's `betweenness` counts ordered pairs, so on an undirected graph it is twice the NetworkX
|
|
1348
|
+
// convention `score` follows. The result does not say which scale it is in, so the factor is rebuilt here
|
|
1349
|
+
// from the rules in @graphty/algorithms' BetweennessOptions.normalized.
|
|
1350
|
+
const n = c.s.nodeCount;
|
|
1351
|
+
const pairs = o.endpoints === true ? n * (n - 1) : (n - 1) * (n - 2);
|
|
1352
|
+
const unordered = c.s.directed ? 1 : 2;
|
|
1353
|
+
const toOrderedPairs = o.normalized === true && pairs > 0 ? pairs : unordered;
|
|
1354
|
+
const raw = Float64Array.from(r.scores, (x) => x * toOrderedPairs);
|
|
1355
|
+
const max = raw.reduce((m, x) => Math.max(m, x), 0);
|
|
1356
|
+
return scored(c, r.scores, {
|
|
1357
|
+
betweenness: accessor(c, raw),
|
|
1358
|
+
betweennessNormalized: accessor(
|
|
1359
|
+
c,
|
|
1360
|
+
raw.map((x) => (max === 0 ? 0 : x / max)),
|
|
1361
|
+
),
|
|
1362
|
+
});
|
|
1363
|
+
});
|
|
1364
|
+
},
|
|
1365
|
+
edgeBetweennessCentrality: (
|
|
1366
|
+
c: Ctx,
|
|
1367
|
+
o: AlgorithmOptions &
|
|
1368
|
+
Sampled & {
|
|
1369
|
+
/** Divide by `(n - 1)(n - 2)` on a directed graph, half that on an undirected one. Default false. */
|
|
1370
|
+
readonly normalized?: boolean;
|
|
1371
|
+
/**
|
|
1372
|
+
* Draw this many source nodes instead of using every node; the same count draws the same nodes. Not
|
|
1373
|
+
* rescaled (see `sources`).
|
|
1374
|
+
*/
|
|
1375
|
+
readonly k?: number;
|
|
1376
|
+
} = {},
|
|
1377
|
+
): MaybeAsync<ScoreResult> => {
|
|
1378
|
+
const sources = o.sources === undefined ? undefined : indicesOf(c.cs, o.sources);
|
|
1379
|
+
return then(c.call("edgeBetweennessCentrality", c.s, { ...c.rest, sources }), (r) =>
|
|
1380
|
+
scored(c, r.scores, {}, true),
|
|
1381
|
+
);
|
|
1382
|
+
},
|
|
1383
|
+
|
|
1384
|
+
// Components and structure
|
|
1385
|
+
connectedComponents: (c: Ctx, _o: AlgorithmOptions = {}): MaybeAsync<Partition> =>
|
|
1386
|
+
then(c.call("connectedComponents", c.s), (r) => partition(c, r.labels, {})),
|
|
1387
|
+
weaklyConnectedComponents: (c: Ctx, _o: AlgorithmOptions = {}): MaybeAsync<Partition> =>
|
|
1388
|
+
then(c.call("weaklyConnectedComponents", c.s), (r) => partition(c, r.labels, {})),
|
|
1389
|
+
stronglyConnectedComponents: (c: Ctx, _o: AlgorithmOptions = {}): Partition =>
|
|
1390
|
+
partition(c, stronglyConnectedComponents(c.s).labels, {}),
|
|
1391
|
+
condensation: (c: Ctx, _o: AlgorithmOptions = {}): Partition<{ condensed: DerivedGraph }> => {
|
|
1392
|
+
const r = condensation(c.s);
|
|
1393
|
+
return partition(c, r.components.labels, { condensed: r.condensed });
|
|
1394
|
+
},
|
|
1395
|
+
kCoreDecomposition: (
|
|
1396
|
+
c: Ctx,
|
|
1397
|
+
_o: AlgorithmOptions = {},
|
|
1398
|
+
): ScoreResult & { maxCore: number; core(k: number): NodeCollection } => {
|
|
1399
|
+
const r = kCoreDecomposition(c.s);
|
|
1400
|
+
const core = (k: number): NodeCollection =>
|
|
1401
|
+
nodesAt(
|
|
1402
|
+
c,
|
|
1403
|
+
Array.from(r.coreness.keys()).filter((i) => r.coreness[i] >= k),
|
|
1404
|
+
);
|
|
1405
|
+
return scored(c, r.coreness, { maxCore: r.maxCore, core });
|
|
1406
|
+
},
|
|
1407
|
+
triangleCount: (
|
|
1408
|
+
c: Ctx,
|
|
1409
|
+
_o: AlgorithmOptions = {},
|
|
1410
|
+
): MaybeAsync<
|
|
1411
|
+
ScoreResult & { coefficient(node: ElementRef): number | undefined; total: number; transitivity: number }
|
|
1412
|
+
> =>
|
|
1413
|
+
then(c.call("triangleCount", c.s), (r) =>
|
|
1414
|
+
scored(c, r.perNode, {
|
|
1415
|
+
coefficient: accessor(c, r.coefficient),
|
|
1416
|
+
total: r.total,
|
|
1417
|
+
transitivity: r.transitivity,
|
|
1418
|
+
}),
|
|
1419
|
+
),
|
|
1420
|
+
isBipartite: (
|
|
1421
|
+
c: Ctx,
|
|
1422
|
+
_o: AlgorithmOptions & BipartiteOptions = {},
|
|
1423
|
+
): { bipartite: boolean; partitionFirst: NodeCollection; partitionSecond: NodeCollection } => {
|
|
1424
|
+
const r = isBipartite(c.s, c.rest);
|
|
1425
|
+
if (r.sides === null) {
|
|
1426
|
+
const empty = c.cy.collection() as unknown as NodeCollection;
|
|
1427
|
+
return { bipartite: false, partitionFirst: empty, partitionSecond: empty };
|
|
1428
|
+
}
|
|
1429
|
+
const { partitionFirst, partitionSecond } = cutOf(c, r.sides, [], 0);
|
|
1430
|
+
return { bipartite: true, partitionFirst, partitionSecond };
|
|
1431
|
+
},
|
|
1432
|
+
isGraphIsomorphic: (
|
|
1433
|
+
c: Ctx,
|
|
1434
|
+
o: IsomorphismOptions,
|
|
1435
|
+
): { isomorphic: boolean; mapping(node: ElementRef): NodeSingular | undefined } => {
|
|
1436
|
+
const other = otherOf(c, o);
|
|
1437
|
+
const r = isGraphIsomorphic(c.s, other.snapshot, isoOptions(c, other, o));
|
|
1438
|
+
const m = r.mapping;
|
|
1439
|
+
return { isomorphic: r.isomorphic, mapping: m === null ? () => undefined : mapper(c, other, m) };
|
|
1440
|
+
},
|
|
1441
|
+
findAllIsomorphisms: (c: Ctx, o: IsomorphismOptions): ((node: ElementRef) => NodeSingular | undefined)[] => {
|
|
1442
|
+
const other = otherOf(c, o);
|
|
1443
|
+
return findAllIsomorphisms(c.s, other.snapshot, isoOptions(c, other, o)).map((m) => mapper(c, other, m));
|
|
1444
|
+
},
|
|
1445
|
+
modularity: (
|
|
1446
|
+
c: Ctx,
|
|
1447
|
+
o: AlgorithmOptions &
|
|
1448
|
+
Omit<ModularityOptions, "weights"> & {
|
|
1449
|
+
/** The clusters (a partition result, or selections), or the node data field holding each node's cluster. */
|
|
1450
|
+
readonly clusters: string | readonly NodeSelection[];
|
|
1451
|
+
},
|
|
1452
|
+
): number => modularity(c.s, labelsOf(c, o.clusters, "clusters"), c.rest),
|
|
1453
|
+
|
|
1454
|
+
// Community detection and clustering
|
|
1455
|
+
louvain: (
|
|
1456
|
+
c: Ctx,
|
|
1457
|
+
_o: AlgorithmOptions & LouvainOptions = {},
|
|
1458
|
+
): Partition<{ modularity: number; iterations: number }> => {
|
|
1459
|
+
const r = louvain(c.s, c.rest);
|
|
1460
|
+
return partition(c, r.labels, { modularity: r.modularity, iterations: r.iterations });
|
|
1461
|
+
},
|
|
1462
|
+
leiden: (
|
|
1463
|
+
c: Ctx,
|
|
1464
|
+
_o: AlgorithmOptions & LeidenOptions = {},
|
|
1465
|
+
): Partition<{ modularity: number; iterations: number }> => {
|
|
1466
|
+
const r = leiden(c.s, c.rest);
|
|
1467
|
+
return partition(c, r.labels, { modularity: r.modularity, iterations: r.iterations });
|
|
1468
|
+
},
|
|
1469
|
+
labelPropagation: (c: Ctx, _o: WeightedFlag<LabelPropagationOptions> = {}): MaybeAsync<Partition<LpaReport>> =>
|
|
1470
|
+
then(c.call("labelPropagation", c.s, { ...c.rest, weighted: c.weighted }), (r) => propagated(c, r)),
|
|
1471
|
+
labelPropagationSynchronous: (
|
|
1472
|
+
c: Ctx,
|
|
1473
|
+
_o: AlgorithmOptions & {
|
|
1474
|
+
/** Iteration cap. */
|
|
1475
|
+
readonly maxIterations?: number;
|
|
1476
|
+
} = {},
|
|
1477
|
+
): MaybeAsync<Partition<LpaReport>> =>
|
|
1478
|
+
then(c.call("labelPropagationSynchronous", c.s, { ...c.rest, weighted: c.weighted }), (r) => propagated(c, r)),
|
|
1479
|
+
labelPropagationSemiSupervised: (
|
|
1480
|
+
c: Ctx,
|
|
1481
|
+
o: WeightedFlag<LabelPropagationOptions> & {
|
|
1482
|
+
/**
|
|
1483
|
+
* Seed labels: the node data field holding them (nodes without it are free), or one selection per label.
|
|
1484
|
+
* Every free node starts with a label of its own, so a seed label spreads only where its seeds outvote
|
|
1485
|
+
* their neighbors: a single seed in a dense group can end up alone in its own cluster.
|
|
1486
|
+
*/
|
|
1487
|
+
readonly seeds: string | readonly NodeSelection[];
|
|
1488
|
+
},
|
|
1489
|
+
): Partition<{ iterations: number; converged: boolean }> => {
|
|
1490
|
+
const r = labelPropagationSemiSupervised(c.s, labelsOf(c, o.seeds, "seeds"), {
|
|
1491
|
+
...c.rest,
|
|
1492
|
+
weighted: c.weighted,
|
|
1493
|
+
});
|
|
1494
|
+
return partition(c, r.labels, { iterations: r.iterations, converged: r.converged });
|
|
1495
|
+
},
|
|
1496
|
+
girvanNewman: (
|
|
1497
|
+
c: Ctx,
|
|
1498
|
+
_o: AlgorithmOptions & GirvanNewmanOptions = {},
|
|
1499
|
+
): Partition<{
|
|
1500
|
+
modularity: number;
|
|
1501
|
+
levels: number;
|
|
1502
|
+
level(k: number): Partition;
|
|
1503
|
+
modularities: number[];
|
|
1504
|
+
}> => {
|
|
1505
|
+
const r = girvanNewman(c.s, c.rest);
|
|
1506
|
+
const quiet: Ctx = { ...c, field: undefined };
|
|
1507
|
+
const level = (k: number): Partition => partition(quiet, r.levels[k] ?? [], {});
|
|
1508
|
+
const best = bestLevel(r.modularity);
|
|
1509
|
+
return partition(c, r.levels[best] ?? [], {
|
|
1510
|
+
modularity: r.modularity[best] ?? Number.NaN,
|
|
1511
|
+
levels: r.levels.length,
|
|
1512
|
+
level,
|
|
1513
|
+
modularities: Array.from(r.modularity),
|
|
1514
|
+
});
|
|
1515
|
+
},
|
|
1516
|
+
markovClustering: (
|
|
1517
|
+
c: Ctx,
|
|
1518
|
+
_o: AlgorithmOptions & Omit<MarkovOptions, "weights"> = {},
|
|
1519
|
+
): Partition<{ iterations: number; converged: boolean }> => {
|
|
1520
|
+
const r = markovClustering(c.s, c.rest);
|
|
1521
|
+
return partition(c, r.labels, { iterations: r.iterations, converged: r.converged });
|
|
1522
|
+
},
|
|
1523
|
+
spectralClustering: (
|
|
1524
|
+
c: Ctx,
|
|
1525
|
+
o: AlgorithmOptions & Omit<SpectralOptions, "weights">,
|
|
1526
|
+
): Partition<{ converged: boolean; eigenvalues: Float64Array }> => {
|
|
1527
|
+
// required by the type, checked for callers without types
|
|
1528
|
+
if ((o.k as number | undefined) === undefined) {
|
|
1529
|
+
throw new Error(`${c.name}: the k option is required`);
|
|
1530
|
+
}
|
|
1531
|
+
const r = spectralClustering(c.s, c.rest as unknown as SpectralOptions);
|
|
1532
|
+
return partition(c, r.labels, { converged: r.converged, eigenvalues: r.eigenvalues });
|
|
1533
|
+
},
|
|
1534
|
+
hierarchicalClustering: (
|
|
1535
|
+
c: Ctx,
|
|
1536
|
+
_o: AlgorithmOptions & HierarchicalOptions = {},
|
|
1537
|
+
): { cut(height: number): Partition; readonly merges: number } => {
|
|
1538
|
+
const r = hierarchicalClustering(c.s, c.rest);
|
|
1539
|
+
const quiet: Ctx = { ...c, field: undefined };
|
|
1540
|
+
const cut = (h: number): Partition => {
|
|
1541
|
+
const labels = new Uint32Array(c.s.nodeCount).fill(INVALID_INDEX);
|
|
1542
|
+
r.cut(h).forEach((group, k) => {
|
|
1543
|
+
for (const i of group) {
|
|
1544
|
+
labels[i] = k;
|
|
1545
|
+
}
|
|
1546
|
+
});
|
|
1547
|
+
return partition(quiet, labels, {});
|
|
1548
|
+
};
|
|
1549
|
+
return { merges: r.left.length, cut };
|
|
1550
|
+
},
|
|
1551
|
+
teraHAC: (c: Ctx, _o: AlgorithmOptions & TeraHacOptions = {}): Partition<{ merges: number }> => {
|
|
1552
|
+
const r = teraHAC(c.s, c.rest);
|
|
1553
|
+
return partition(c, r.labels, { merges: r.merges });
|
|
1554
|
+
},
|
|
1555
|
+
grsbm: (c: Ctx, _o: WeightedFlag<GrsbmOptions> = {}): Partition =>
|
|
1556
|
+
partition(c, grsbm(c.s, { ...c.rest, weighted: c.weighted }).labels, {}),
|
|
1557
|
+
syncClustering: (
|
|
1558
|
+
c: Ctx,
|
|
1559
|
+
o: AlgorithmOptions & SyncClusteringOptions,
|
|
1560
|
+
): Partition<{ loss: number; iterations: number; converged: boolean }> => {
|
|
1561
|
+
// required by the type, checked for callers without types
|
|
1562
|
+
if ((o.numClusters as number | undefined) === undefined) {
|
|
1563
|
+
throw new Error(`${c.name}: the numClusters option is required`);
|
|
1564
|
+
}
|
|
1565
|
+
const r = syncClustering(c.s, c.rest as unknown as SyncClusteringOptions);
|
|
1566
|
+
return partition(c, r.labels, { loss: r.loss, iterations: r.iterations, converged: r.converged });
|
|
1567
|
+
},
|
|
1568
|
+
|
|
1569
|
+
// Spanning trees, flow, cuts, matching
|
|
1570
|
+
kruskalMST: (c: Ctx, _o: AlgorithmOptions = {}): CollectionReturnValue & { totalWeight: number } => {
|
|
1571
|
+
const r = kruskalMST(c.s);
|
|
1572
|
+
return Object.assign(c.cs.nodes.union(edgesAt(c, r.edges)), { totalWeight: r.totalWeight });
|
|
1573
|
+
},
|
|
1574
|
+
primMST: (
|
|
1575
|
+
c: Ctx,
|
|
1576
|
+
o: AlgorithmOptions & {
|
|
1577
|
+
/** The node the tree grows from; default the first node. */
|
|
1578
|
+
readonly root?: NodeSelection;
|
|
1579
|
+
/** Grow a tree in every component instead of throwing on a disconnected graph. Default false. */
|
|
1580
|
+
readonly forest?: boolean;
|
|
1581
|
+
} = {},
|
|
1582
|
+
): CollectionReturnValue & { totalWeight: number } => {
|
|
1583
|
+
const start = indexOf(c.cs, o.root, `${c.name}: root`);
|
|
1584
|
+
const r = primMST(c.s, { ...c.rest, start });
|
|
1585
|
+
// a run that does not throw spans every node: the graph is connected, or `forest` grew a tree in every
|
|
1586
|
+
// component (an isolated node is a tree of its own)
|
|
1587
|
+
return Object.assign(c.cs.nodes.union(edgesAt(c, r.edges)), { totalWeight: r.totalWeight });
|
|
1588
|
+
},
|
|
1589
|
+
maxFlow: (c: Ctx, o: FlowOptions): CutResult & { flow(edge: ElementRef): number | undefined } => {
|
|
1590
|
+
const r = maxFlow(c.s, req(c, o.source, "source"), req(c, o.sink, "sink"), c.rest);
|
|
1591
|
+
return { ...cutOf(c, r.sourceSide, r.cutEdges, r.maxFlow), flow: accessor(c, r.flow, true) };
|
|
1592
|
+
},
|
|
1593
|
+
minSTCut: (c: Ctx, o: FlowOptions): CutResult =>
|
|
1594
|
+
minCut(c, minSTCut(c.s, req(c, o.source, "source"), req(c, o.sink, "sink"), c.rest)),
|
|
1595
|
+
stoerWagner: (c: Ctx, _o: AlgorithmOptions = {}): CutResult => minCut(c, stoerWagner(c.s)),
|
|
1596
|
+
kargerMinCut: (c: Ctx, _o: AlgorithmOptions & Omit<KargerOptions, "weights"> = {}): CutResult =>
|
|
1597
|
+
minCut(c, kargerMinCut(c.s, c.rest)),
|
|
1598
|
+
maximumBipartiteMatching: (c: Ctx, o: MatchingOptions = {}): MatchingResult =>
|
|
1599
|
+
matched(c, maximumBipartiteMatching(c.s, { ...c.rest, left: maskOf(c, o.left), right: maskOf(c, o.right) })),
|
|
1600
|
+
greedyBipartiteMatching: (c: Ctx, o: MatchingOptions = {}): MatchingResult =>
|
|
1601
|
+
matched(c, greedyBipartiteMatching(c.s, { ...c.rest, left: maskOf(c, o.left), right: maskOf(c, o.right) })),
|
|
1602
|
+
|
|
1603
|
+
// Link prediction. `directed` also selects the directed score (out-neighbours of u against in-neighbours of v).
|
|
1604
|
+
commonNeighborsScore: (c: Ctx, o: AlgorithmOptions & PairOptions): number =>
|
|
1605
|
+
commonNeighborsScore(c.s, req(c, o.source, "source"), req(c, o.target, "target"), { directed: c.s.directed }),
|
|
1606
|
+
adamicAdarScore: (c: Ctx, o: AlgorithmOptions & PairOptions): number =>
|
|
1607
|
+
adamicAdarScore(c.s, req(c, o.source, "source"), req(c, o.target, "target"), { directed: c.s.directed }),
|
|
1608
|
+
commonNeighborsPrediction: (
|
|
1609
|
+
c: Ctx,
|
|
1610
|
+
_o: AlgorithmOptions & Omit<LinkPredictionOptions, "directed"> = {},
|
|
1611
|
+
): PredictedLink[] => links(c, commonNeighborsPrediction(c.s, { ...c.rest, directed: c.s.directed })),
|
|
1612
|
+
adamicAdarPrediction: (
|
|
1613
|
+
c: Ctx,
|
|
1614
|
+
_o: AlgorithmOptions & Omit<LinkPredictionOptions, "directed"> = {},
|
|
1615
|
+
): PredictedLink[] => links(c, adamicAdarPrediction(c.s, { ...c.rest, directed: c.s.directed })),
|
|
1616
|
+
commonNeighborsForPairs: (c: Ctx, o: AlgorithmOptions & PairsOptions): number[] =>
|
|
1617
|
+
Array.from(commonNeighborsForPairs(c.s, pairsOf(c, o.pairs, "pairs"), { directed: c.s.directed })),
|
|
1618
|
+
adamicAdarForPairs: (c: Ctx, o: AlgorithmOptions & PairsOptions): number[] =>
|
|
1619
|
+
Array.from(adamicAdarForPairs(c.s, pairsOf(c, o.pairs, "pairs"), { directed: c.s.directed })),
|
|
1620
|
+
topCandidatesForNode: (
|
|
1621
|
+
c: Ctx,
|
|
1622
|
+
o: Rooted & Omit<CandidateOptions, "directed" | "candidates"> & Candidates,
|
|
1623
|
+
): PredictedLink[] => {
|
|
1624
|
+
const candidates = o.candidates === undefined ? undefined : indicesOf(c.cs, o.candidates);
|
|
1625
|
+
return links(
|
|
1626
|
+
c,
|
|
1627
|
+
getTopCandidatesForNode(c.s, req(c, o.root, "root"), {
|
|
1628
|
+
...c.rest,
|
|
1629
|
+
topK: topK(o),
|
|
1630
|
+
candidates,
|
|
1631
|
+
directed: c.s.directed,
|
|
1632
|
+
}),
|
|
1633
|
+
);
|
|
1634
|
+
},
|
|
1635
|
+
topAdamicAdarCandidatesForNode: (
|
|
1636
|
+
c: Ctx,
|
|
1637
|
+
o: Rooted & Omit<CandidateOptions, "directed" | "candidates"> & Candidates,
|
|
1638
|
+
): PredictedLink[] => {
|
|
1639
|
+
const candidates = o.candidates === undefined ? undefined : indicesOf(c.cs, o.candidates);
|
|
1640
|
+
return links(
|
|
1641
|
+
c,
|
|
1642
|
+
getTopAdamicAdarCandidatesForNode(c.s, req(c, o.root, "root"), {
|
|
1643
|
+
...c.rest,
|
|
1644
|
+
topK: topK(o),
|
|
1645
|
+
candidates,
|
|
1646
|
+
directed: c.s.directed,
|
|
1647
|
+
}),
|
|
1648
|
+
);
|
|
1649
|
+
},
|
|
1650
|
+
evaluateCommonNeighbors: (c: Ctx, o: EvaluateOptions): LinkPredictionMetrics =>
|
|
1651
|
+
evaluateCommonNeighbors(c.s, pairsOf(c, o.edges, "edges"), pairsOf(c, o.nonEdges, "nonEdges"), {
|
|
1652
|
+
directed: c.s.directed,
|
|
1653
|
+
}),
|
|
1654
|
+
evaluateAdamicAdar: (c: Ctx, o: EvaluateOptions): LinkPredictionMetrics =>
|
|
1655
|
+
evaluateAdamicAdar(c.s, pairsOf(c, o.edges, "edges"), pairsOf(c, o.nonEdges, "nonEdges"), {
|
|
1656
|
+
directed: c.s.directed,
|
|
1657
|
+
}),
|
|
1658
|
+
compareAdamicAdarWithCommonNeighbors: (
|
|
1659
|
+
c: Ctx,
|
|
1660
|
+
o: EvaluateOptions,
|
|
1661
|
+
): { adamicAdar: LinkPredictionMetrics; commonNeighbors: LinkPredictionMetrics } => {
|
|
1662
|
+
// both halves with `directed` (@graphty/algorithms' compareAdamicAdarWithCommonNeighbors drops it for the
|
|
1663
|
+
// common-neighbors half), so each matches the method of its own name
|
|
1664
|
+
const edges = pairsOf(c, o.edges, "edges");
|
|
1665
|
+
const nonEdges = pairsOf(c, o.nonEdges, "nonEdges");
|
|
1666
|
+
const opts = { directed: c.s.directed };
|
|
1667
|
+
return {
|
|
1668
|
+
adamicAdar: evaluateAdamicAdar(c.s, edges, nonEdges, opts),
|
|
1669
|
+
commonNeighbors: evaluateCommonNeighbors(c.s, edges, nonEdges, opts),
|
|
1670
|
+
};
|
|
1671
|
+
},
|
|
1672
|
+
};
|
|
1673
|
+
|
|
1674
|
+
// ---------------------------------------------------------------------------------------------------------------
|
|
1675
|
+
// Registration
|
|
1676
|
+
|
|
1677
|
+
type Impls = typeof IMPLS;
|
|
1678
|
+
type Tail<T extends readonly unknown[]> = T extends readonly [unknown, ...infer R] ? R : never;
|
|
1679
|
+
|
|
1680
|
+
/** The methods `cytoscape.use(graphtyCytoscape)` adds to every collection and to the core. */
|
|
1681
|
+
export type GraphtyAlgorithms = {
|
|
1682
|
+
[K in keyof Impls as `graphty${Capitalize<K>}`]: (
|
|
1683
|
+
...options: Tail<Parameters<Impls[K]>>
|
|
1684
|
+
) => Awaited<ReturnType<Impls[K]>>;
|
|
1685
|
+
} & {
|
|
1686
|
+
[K in AsyncKey as `graphty${Capitalize<K>}Async`]: (
|
|
1687
|
+
...options: Tail<Parameters<Impls[K]>>
|
|
1688
|
+
) => Promise<Awaited<ReturnType<Impls[K]>> & { readonly backend: Backend }>;
|
|
1689
|
+
};
|
|
1690
|
+
|
|
1691
|
+
// Module augmentation is how a Cytoscape extension types its methods. The interfaces are empty because the members
|
|
1692
|
+
// come from the merged-in supertype, which is what the lint rule cannot see.
|
|
1693
|
+
declare module "cytoscape" {
|
|
1694
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type -- declaration merging, see above
|
|
1695
|
+
interface CollectionAlgorithms extends GraphtyAlgorithms {}
|
|
1696
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type -- declaration merging, see above
|
|
1697
|
+
interface Core extends GraphtyAlgorithms {}
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
/**
|
|
1701
|
+
* The method name of an algorithm.
|
|
1702
|
+
* @param key - the @graphty/algorithms name
|
|
1703
|
+
* @returns "graphty" + the name with a capital
|
|
1704
|
+
*/
|
|
1705
|
+
function methodName(key: string): string {
|
|
1706
|
+
return `graphty${key.charAt(0).toUpperCase()}${key.slice(1)}`;
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
/** Every synchronous method name `cytoscape.use(graphtyCytoscape)` registers on collections and the core. */
|
|
1710
|
+
export const ALGORITHM_NAMES: readonly string[] = Object.keys(IMPLS).map(methodName);
|
|
1711
|
+
|
|
1712
|
+
// The algorithms whose implementation goes through `c.call`, i.e. those the dispatcher can send to the GPU.
|
|
1713
|
+
const ASYNC_KEYS = [
|
|
1714
|
+
"breadthFirstSearch",
|
|
1715
|
+
"dijkstra",
|
|
1716
|
+
"bellmanFord",
|
|
1717
|
+
"allPairsShortestPath",
|
|
1718
|
+
"pageRank",
|
|
1719
|
+
"personalizedPageRank",
|
|
1720
|
+
"eigenvectorCentrality",
|
|
1721
|
+
"katzCentrality",
|
|
1722
|
+
"hits",
|
|
1723
|
+
"closenessCentrality",
|
|
1724
|
+
"betweennessCentrality",
|
|
1725
|
+
"edgeBetweennessCentrality",
|
|
1726
|
+
"connectedComponents",
|
|
1727
|
+
"weaklyConnectedComponents",
|
|
1728
|
+
"triangleCount",
|
|
1729
|
+
"labelPropagation",
|
|
1730
|
+
"labelPropagationSynchronous",
|
|
1731
|
+
] as const satisfies readonly (keyof Impls)[];
|
|
1732
|
+
|
|
1733
|
+
/** The algorithms with an `...Async` method: those the WebGPU accelerator can run. */
|
|
1734
|
+
type AsyncKey = (typeof ASYNC_KEYS)[number];
|
|
1735
|
+
|
|
1736
|
+
/** Every `...Async` method name: the algorithms the WebGPU accelerator can run. */
|
|
1737
|
+
export const ASYNC_ALGORITHM_NAMES: readonly string[] = ASYNC_KEYS.map((k) => `${methodName(k)}Async`);
|
|
1738
|
+
|
|
1739
|
+
/** The algorithm table a run calls through: the CPU functions, or the dispatcher. */
|
|
1740
|
+
type Algos = Record<GpuKey, (...args: never[]) => unknown>;
|
|
1741
|
+
|
|
1742
|
+
/**
|
|
1743
|
+
* Runs one algorithm over a collection.
|
|
1744
|
+
* @param eles - the collection
|
|
1745
|
+
* @param key - the algorithm
|
|
1746
|
+
* @param options - the caller's options
|
|
1747
|
+
* @param algos - the CPU functions (a synchronous run) or the dispatcher (an asynchronous one)
|
|
1748
|
+
* @returns the algorithm's result (a promise through the dispatcher) and the snapshot it ran on
|
|
1749
|
+
*/
|
|
1750
|
+
function run(
|
|
1751
|
+
eles: Collection,
|
|
1752
|
+
key: keyof Impls,
|
|
1753
|
+
options: AlgorithmOptions,
|
|
1754
|
+
algos: Algos,
|
|
1755
|
+
): { result: unknown; snapshot: GraphSnapshot } {
|
|
1756
|
+
const name = methodName(key);
|
|
1757
|
+
// a Cytoscape built-in takes some arguments positionally (kruskal(weightFn)); one ignored here would run unweighted
|
|
1758
|
+
if (typeof options !== "object" || options === null || Array.isArray(options)) {
|
|
1759
|
+
throw new TypeError(`${name}: takes one options object; pass a weight function as { weight: fn }`);
|
|
1760
|
+
}
|
|
1761
|
+
if (options.weight !== undefined && UNWEIGHTED.has(key)) {
|
|
1762
|
+
throw new Error(`${name}: this algorithm reads no edge weights; remove the weight option`);
|
|
1763
|
+
}
|
|
1764
|
+
if (options.field !== undefined && NO_FIELD.has(key)) {
|
|
1765
|
+
throw new Error(`${name}: this algorithm has no per-element value to write; remove the field option`);
|
|
1766
|
+
}
|
|
1767
|
+
const known = OPTION_NAMES[name] ?? [];
|
|
1768
|
+
for (const [k, v] of Object.entries(options)) {
|
|
1769
|
+
if (v !== undefined && !known.includes(k)) {
|
|
1770
|
+
throw new Error(`${name}: unknown option ${k}; the options are ${known.join(", ")}`);
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
const directed = options.directed ?? DIRECTED.has(key);
|
|
1774
|
+
// degree and eigenvector centrality read mode only on a directed graph; ignored, it would give total degree
|
|
1775
|
+
const { mode } = options as { mode?: unknown };
|
|
1776
|
+
if ((mode === "in" || mode === "out") && !directed) {
|
|
1777
|
+
throw new Error(`${name}: mode: "${mode}" needs directed: true`);
|
|
1778
|
+
}
|
|
1779
|
+
const cs = toSnapshot(eles, { directed, weight: options.weight });
|
|
1780
|
+
const rest = {
|
|
1781
|
+
...DEFAULTS[key],
|
|
1782
|
+
...Object.fromEntries(Object.entries(options).filter(([k, v]) => !ADAPTER_KEYS.has(k) && v !== undefined)),
|
|
1783
|
+
};
|
|
1784
|
+
const c: Ctx = {
|
|
1785
|
+
name,
|
|
1786
|
+
cy: coreOf(eles),
|
|
1787
|
+
cs,
|
|
1788
|
+
s: cs.snapshot,
|
|
1789
|
+
field: options.field,
|
|
1790
|
+
weighted: options.weight !== undefined,
|
|
1791
|
+
rest,
|
|
1792
|
+
call: (k, ...args) => (algos[k] as (...a: unknown[]) => never)(...args),
|
|
1793
|
+
};
|
|
1794
|
+
const fail = (error: unknown): never => {
|
|
1795
|
+
throw named(error, key);
|
|
1796
|
+
};
|
|
1797
|
+
let result: unknown;
|
|
1798
|
+
try {
|
|
1799
|
+
result = (IMPLS[key] as (c: Ctx, o: AlgorithmOptions) => unknown)(c, options);
|
|
1800
|
+
} catch (error) {
|
|
1801
|
+
fail(error);
|
|
1802
|
+
}
|
|
1803
|
+
return { result: result instanceof Promise ? result.catch(fail) : result, snapshot: cs.snapshot };
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
/**
|
|
1807
|
+
* An algorithm's error in a Cytoscape user's terms: it starts with the method name, and a graph of the wrong
|
|
1808
|
+
* direction is told which option to change (the @graphty/algorithms message names its own functions and snapshot
|
|
1809
|
+
* methods). The error object, its class and its `code` are kept.
|
|
1810
|
+
* @param error - what the algorithm threw
|
|
1811
|
+
* @param key - the algorithm
|
|
1812
|
+
* @returns the error to throw
|
|
1813
|
+
*/
|
|
1814
|
+
function named(error: unknown, key: keyof Impls): unknown {
|
|
1815
|
+
if (!(error instanceof Error)) {
|
|
1816
|
+
return error;
|
|
1817
|
+
}
|
|
1818
|
+
const name = methodName(key);
|
|
1819
|
+
const { code } = error as { code?: unknown };
|
|
1820
|
+
if (code === "E_NEEDS_UNDIRECTED") {
|
|
1821
|
+
const instead = key === "connectedComponents" ? ", or call graphtyWeaklyConnectedComponents" : "";
|
|
1822
|
+
error.message = `${name}: needs an undirected graph; leave out directed: true${instead}`;
|
|
1823
|
+
} else if (code === "E_NEEDS_DIRECTED") {
|
|
1824
|
+
error.message = `${name}: needs a directed graph; ${DIRECTED.has(key) ? "leave out directed: false" : "pass directed: true"}`;
|
|
1825
|
+
} else if (!error.message.startsWith(`${name}:`)) {
|
|
1826
|
+
error.message = `${name}: ${error.message}`;
|
|
1827
|
+
}
|
|
1828
|
+
return error;
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
/**
|
|
1832
|
+
* Runs an algorithm synchronously on the CPU.
|
|
1833
|
+
* @param eles - the collection
|
|
1834
|
+
* @param key - the algorithm
|
|
1835
|
+
* @param options - the caller's options
|
|
1836
|
+
* @returns the result
|
|
1837
|
+
*/
|
|
1838
|
+
function runSync(eles: Collection, key: keyof Impls, options: AlgorithmOptions = {}): unknown {
|
|
1839
|
+
checkGpuMode(methodName(key), options?.gpu);
|
|
1840
|
+
if (options.gpu === "require") {
|
|
1841
|
+
const name = methodName(key);
|
|
1842
|
+
throw new Error(
|
|
1843
|
+
(ASYNC_KEYS as readonly string[]).includes(key)
|
|
1844
|
+
? `${name}: runs on the CPU; call ${name}Async for the GPU`
|
|
1845
|
+
: `${name}: has no GPU implementation and runs only on the CPU; leave out gpu: "require"`,
|
|
1846
|
+
);
|
|
1847
|
+
}
|
|
1848
|
+
return run(eles, key, options, CPU).result;
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
/**
|
|
1852
|
+
* Runs an algorithm through the @graphty/algorithms dispatcher, on the core's GPU when it has one, and reports
|
|
1853
|
+
* which implementation answered. A GPU failure after the run started is thrown, never finished on the CPU.
|
|
1854
|
+
* @param eles - the collection
|
|
1855
|
+
* @param key - the algorithm
|
|
1856
|
+
* @param options - the caller's options
|
|
1857
|
+
* @returns the result with `backend`
|
|
1858
|
+
*/
|
|
1859
|
+
async function runAsync(eles: Collection, key: keyof Impls, options: AlgorithmOptions = {}): Promise<unknown> {
|
|
1860
|
+
checkGpuMode(`${methodName(key)}Async`, options?.gpu);
|
|
1861
|
+
const d = await gpuFor(coreOf(eles), options.gpu);
|
|
1862
|
+
const rec = d.gpu === null ? null : recording(d.gpu.accelerator);
|
|
1863
|
+
warnIfFixable(d, eles.nodes().length);
|
|
1864
|
+
const { result, snapshot } = run(eles, key, options, accelerated(rec?.accelerator ?? null));
|
|
1865
|
+
// a data change during the run (another run writing its field, say) must not release what this run reads
|
|
1866
|
+
const unhold = hold(snapshot);
|
|
1867
|
+
let value: object;
|
|
1868
|
+
try {
|
|
1869
|
+
value = (await result) as object;
|
|
1870
|
+
} finally {
|
|
1871
|
+
unhold();
|
|
1872
|
+
if (d.gpu !== null && typeof options.weight === "function") {
|
|
1873
|
+
// a weight function bypasses the snapshot cache, so nothing else would release this upload
|
|
1874
|
+
d.gpu.accelerator.release(snapshot);
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
const backend = backendOf(
|
|
1878
|
+
d,
|
|
1879
|
+
rec?.used() ?? false,
|
|
1880
|
+
"the options or the graph need the CPU implementation (the @graphty/algorithms dispatcher does not say which)",
|
|
1881
|
+
);
|
|
1882
|
+
return Object.assign(value, { backend });
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
/** The registration function Cytoscape's `use()` calls. */
|
|
1886
|
+
type Register = (type: string, name: string, registrant: unknown) => void;
|
|
1887
|
+
|
|
1888
|
+
/**
|
|
1889
|
+
* Registers every algorithm as a collection method and a core method named "graphty<Name>".
|
|
1890
|
+
* @param cytoscape - the cytoscape function
|
|
1891
|
+
*/
|
|
1892
|
+
export function registerAlgorithms(cytoscape: Register): void {
|
|
1893
|
+
for (const key of Object.keys(IMPLS) as (keyof Impls)[]) {
|
|
1894
|
+
const name = methodName(key);
|
|
1895
|
+
cytoscape("collection", name, function (this: Collection, options?: AlgorithmOptions) {
|
|
1896
|
+
return runSync(this, key, options);
|
|
1897
|
+
});
|
|
1898
|
+
cytoscape("core", name, function (this: Core, options?: AlgorithmOptions) {
|
|
1899
|
+
return runSync(this.elements(), key, options);
|
|
1900
|
+
});
|
|
1901
|
+
}
|
|
1902
|
+
for (const key of ASYNC_KEYS) {
|
|
1903
|
+
const name = `${methodName(key)}Async`;
|
|
1904
|
+
cytoscape("collection", name, function (this: Collection, options?: AlgorithmOptions) {
|
|
1905
|
+
return runAsync(this, key, options);
|
|
1906
|
+
});
|
|
1907
|
+
cytoscape("core", name, function (this: Core, options?: AlgorithmOptions) {
|
|
1908
|
+
return runAsync(this.elements(), key, options);
|
|
1909
|
+
});
|
|
1910
|
+
}
|
|
1911
|
+
}
|