@statelyai/graph 1.0.0 → 2.1.0
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/README.md +121 -44
- package/dist/{adjacency-list-VsUaH9SJ.mjs → adjacency-list-DQ32Mmhx.mjs} +3 -1
- package/dist/algorithms-D1cgly0g.d.mts +452 -0
- package/dist/algorithms-DBpH74hR.mjs +3309 -0
- package/dist/algorithms.d.mts +2 -2
- package/dist/algorithms.mjs +2 -2
- package/dist/config-Dt5u1gSf.mjs +793 -0
- package/dist/{converter-udLITX36.mjs → converter-DB6Rg6Vd.mjs} +2 -2
- package/dist/format-support.mjs +38 -11
- package/dist/formats/adjacency-list/index.d.mts +1 -1
- package/dist/formats/adjacency-list/index.mjs +1 -1
- package/dist/formats/converter/index.d.mts +1 -1
- package/dist/formats/converter/index.mjs +1 -1
- package/dist/formats/cytoscape/index.d.mts +4 -4
- package/dist/formats/cytoscape/index.mjs +10 -4
- package/dist/formats/d2/index.d.mts +1 -1
- package/dist/formats/d2/index.mjs +26 -12
- package/dist/formats/d3/index.d.mts +4 -4
- package/dist/formats/d3/index.mjs +10 -4
- package/dist/formats/dot/index.d.mts +1 -1
- package/dist/formats/dot/index.mjs +22 -6
- package/dist/formats/edge-list/index.d.mts +1 -1
- package/dist/formats/edge-list/index.mjs +1 -1
- package/dist/formats/elk/index.d.mts +1 -1
- package/dist/formats/elk/index.mjs +63 -24
- package/dist/formats/gexf/index.d.mts +1 -1
- package/dist/formats/gexf/index.mjs +43 -16
- package/dist/formats/gml/index.d.mts +4 -4
- package/dist/formats/gml/index.mjs +28 -15
- package/dist/formats/graphml/index.d.mts +1 -1
- package/dist/formats/graphml/index.mjs +96 -23
- package/dist/formats/jgf/index.d.mts +4 -4
- package/dist/formats/jgf/index.mjs +12 -5
- package/dist/formats/mermaid/index.d.mts +1 -1
- package/dist/formats/mermaid/index.mjs +49 -12
- package/dist/formats/tgf/index.d.mts +4 -4
- package/dist/formats/tgf/index.mjs +4 -4
- package/dist/formats/xyflow/index.d.mts +12 -6
- package/dist/formats/xyflow/index.mjs +42 -10
- package/dist/{index-D9Kj6Fe3.d.mts → index-BlbSWUvH.d.mts} +1 -1
- package/dist/{index-CHoriXZD.d.mts → index-CNvqxPLJ.d.mts} +157 -30
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +290 -307
- package/dist/layout/cytoscape.d.mts +66 -0
- package/dist/layout/cytoscape.mjs +114 -0
- package/dist/layout/d3-force.d.mts +52 -0
- package/dist/layout/d3-force.mjs +127 -0
- package/dist/layout/d3-hierarchy.d.mts +39 -0
- package/dist/layout/d3-hierarchy.mjs +135 -0
- package/dist/layout/dagre.d.mts +32 -0
- package/dist/layout/dagre.mjs +99 -0
- package/dist/layout/elk.d.mts +47 -0
- package/dist/layout/elk.mjs +73 -0
- package/dist/layout/forceatlas2.d.mts +48 -0
- package/dist/layout/forceatlas2.mjs +100 -0
- package/dist/layout/graphviz.d.mts +50 -0
- package/dist/layout/graphviz.mjs +179 -0
- package/dist/layout/index.d.mts +185 -0
- package/dist/layout/index.mjs +181 -0
- package/dist/layout/webcola.d.mts +40 -0
- package/dist/layout/webcola.mjs +104 -0
- package/dist/{queries-BlkA1HAN.d.mts → queries-B6quF529.d.mts} +43 -12
- package/dist/queries-BMM0XAv_.mjs +986 -0
- package/dist/queries.d.mts +1 -1
- package/dist/queries.mjs +1 -768
- package/dist/schemas.d.mts +19 -1
- package/dist/schemas.mjs +32 -84
- package/dist/{types-3-FS9NV2.d.mts → types-BAEQTwK_.d.mts} +99 -7
- package/dist/validate-BsfSOv0S.mjs +190 -0
- package/package.json +59 -7
- package/schemas/edge.schema.json +27 -0
- package/schemas/graph.schema.json +27 -0
- package/dist/algorithms-Ba7o7niK.mjs +0 -2394
- package/dist/algorithms-fTqmvhzP.d.mts +0 -178
- package/dist/indexing-DR8M1vBy.mjs +0 -137
- /package/dist/{edge-list-DP4otyPU.mjs → edge-list-CA9UTvn2.mjs} +0 -0
- /package/dist/{mode-D8OnHFBk.mjs → mode-gu_mhKKs.mjs} +0 -0
|
@@ -0,0 +1,3309 @@
|
|
|
1
|
+
import { O as getIndex, l as getInDegree, p as getOutDegree, r as getDegree } from "./queries-BMM0XAv_.mjs";
|
|
2
|
+
import { n as toNodeConfig, s as createGraph, t as toEdgeConfig } from "./config-Dt5u1gSf.mjs";
|
|
3
|
+
import { t as getEdgeMode } from "./mode-gu_mhKKs.mjs";
|
|
4
|
+
|
|
5
|
+
//#region src/algorithms/shared.ts
|
|
6
|
+
var MinPriorityQueue = class {
|
|
7
|
+
items = [];
|
|
8
|
+
constructor(compare) {
|
|
9
|
+
this.compare = compare;
|
|
10
|
+
}
|
|
11
|
+
get size() {
|
|
12
|
+
return this.items.length;
|
|
13
|
+
}
|
|
14
|
+
push(item) {
|
|
15
|
+
this.items.push(item);
|
|
16
|
+
this.bubbleUp(this.items.length - 1);
|
|
17
|
+
}
|
|
18
|
+
peek() {
|
|
19
|
+
return this.items[0];
|
|
20
|
+
}
|
|
21
|
+
pop() {
|
|
22
|
+
if (this.items.length === 0) return void 0;
|
|
23
|
+
const first = this.items[0];
|
|
24
|
+
const last = this.items.pop();
|
|
25
|
+
if (this.items.length > 0) {
|
|
26
|
+
this.items[0] = last;
|
|
27
|
+
this.bubbleDown(0);
|
|
28
|
+
}
|
|
29
|
+
return first;
|
|
30
|
+
}
|
|
31
|
+
bubbleUp(index) {
|
|
32
|
+
let current = index;
|
|
33
|
+
while (current > 0) {
|
|
34
|
+
const parent = Math.floor((current - 1) / 2);
|
|
35
|
+
if (this.compare(this.items[current], this.items[parent]) >= 0) break;
|
|
36
|
+
[this.items[current], this.items[parent]] = [this.items[parent], this.items[current]];
|
|
37
|
+
current = parent;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
bubbleDown(index) {
|
|
41
|
+
let current = index;
|
|
42
|
+
while (true) {
|
|
43
|
+
const left = current * 2 + 1;
|
|
44
|
+
const right = left + 1;
|
|
45
|
+
let smallest = current;
|
|
46
|
+
if (left < this.items.length && this.compare(this.items[left], this.items[smallest]) < 0) smallest = left;
|
|
47
|
+
if (right < this.items.length && this.compare(this.items[right], this.items[smallest]) < 0) smallest = right;
|
|
48
|
+
if (smallest === current) break;
|
|
49
|
+
[this.items[current], this.items[smallest]] = [this.items[smallest], this.items[current]];
|
|
50
|
+
current = smallest;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Seeded PRNG (mulberry32). Same generator as src/walks.ts — kept here so
|
|
56
|
+
* algorithm modules and generators can share it without importing walks.
|
|
57
|
+
*/
|
|
58
|
+
function mulberry32(seed) {
|
|
59
|
+
let s = seed | 0;
|
|
60
|
+
return () => {
|
|
61
|
+
s = s + 1831565813 | 0;
|
|
62
|
+
let t = Math.imul(s ^ s >>> 15, 1 | s);
|
|
63
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
64
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Classify a graph by the *effective* mode of its edges (per-edge `mode`
|
|
69
|
+
* overrides included): all-directed, all-non-directed, or genuinely mixed.
|
|
70
|
+
* Edge-less graphs fall back to `graph.mode`.
|
|
71
|
+
*/
|
|
72
|
+
function getEffectiveModeKind(graph) {
|
|
73
|
+
let sawDirected = false;
|
|
74
|
+
let sawNonDirected = false;
|
|
75
|
+
for (const edge of graph.edges) {
|
|
76
|
+
if (getEdgeMode(graph, edge) === "directed") sawDirected = true;
|
|
77
|
+
else sawNonDirected = true;
|
|
78
|
+
if (sawDirected && sawNonDirected) return "mixed";
|
|
79
|
+
}
|
|
80
|
+
if (sawDirected) return "directed";
|
|
81
|
+
if (sawNonDirected) return "non-directed";
|
|
82
|
+
return graph.mode === "directed" ? "directed" : "non-directed";
|
|
83
|
+
}
|
|
84
|
+
function getNeighborIds(graph, nodeId) {
|
|
85
|
+
const idx = getIndex(graph);
|
|
86
|
+
const ids = [];
|
|
87
|
+
for (const eid of idx.outEdges.get(nodeId) ?? []) {
|
|
88
|
+
const ai = idx.edgeById.get(eid);
|
|
89
|
+
if (ai !== void 0) ids.push(graph.edges[ai].targetId);
|
|
90
|
+
}
|
|
91
|
+
for (const eid of idx.inEdges.get(nodeId) ?? []) {
|
|
92
|
+
const ai = idx.edgeById.get(eid);
|
|
93
|
+
if (ai === void 0) continue;
|
|
94
|
+
const edge = graph.edges[ai];
|
|
95
|
+
if (getEdgeMode(graph, edge) !== "directed") ids.push(edge.sourceId);
|
|
96
|
+
}
|
|
97
|
+
return ids;
|
|
98
|
+
}
|
|
99
|
+
function getSuccessorIds(graph, nodeId) {
|
|
100
|
+
const idx = getIndex(graph);
|
|
101
|
+
return (idx.outEdges.get(nodeId) ?? []).map((eid) => graph.edges[idx.edgeById.get(eid)].targetId);
|
|
102
|
+
}
|
|
103
|
+
function resolveFrom(graph, opts) {
|
|
104
|
+
if (opts?.from) return opts.from;
|
|
105
|
+
if (graph.initialNodeId) return graph.initialNodeId;
|
|
106
|
+
const inDeg = /* @__PURE__ */ new Map();
|
|
107
|
+
for (const node of graph.nodes) inDeg.set(node.id, 0);
|
|
108
|
+
for (const edge of graph.edges) inDeg.set(edge.targetId, (inDeg.get(edge.targetId) ?? 0) + 1);
|
|
109
|
+
const roots = [...inDeg.entries()].filter(([, degree]) => degree === 0).map(([id]) => id);
|
|
110
|
+
if (roots.length === 1) return roots[0];
|
|
111
|
+
throw new Error("Cannot determine start node — provide opts.from or set graph.initialNodeId");
|
|
112
|
+
}
|
|
113
|
+
function getNeighborEdges(graph, nodeId) {
|
|
114
|
+
const idx = getIndex(graph);
|
|
115
|
+
const result = [];
|
|
116
|
+
for (const eid of idx.outEdges.get(nodeId) ?? []) {
|
|
117
|
+
const ai = idx.edgeById.get(eid);
|
|
118
|
+
if (ai !== void 0) {
|
|
119
|
+
const edge = graph.edges[ai];
|
|
120
|
+
result.push({
|
|
121
|
+
neighborId: edge.targetId,
|
|
122
|
+
edge
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
for (const eid of idx.inEdges.get(nodeId) ?? []) {
|
|
127
|
+
const ai = idx.edgeById.get(eid);
|
|
128
|
+
if (ai !== void 0) {
|
|
129
|
+
const edge = graph.edges[ai];
|
|
130
|
+
if (getEdgeMode(graph, edge) !== "directed") result.push({
|
|
131
|
+
neighborId: edge.sourceId,
|
|
132
|
+
edge
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
function getNeighborEdgesAll(graph, nodeId) {
|
|
139
|
+
const idx = getIndex(graph);
|
|
140
|
+
const result = [];
|
|
141
|
+
for (const eid of idx.outEdges.get(nodeId) ?? []) {
|
|
142
|
+
const ai = idx.edgeById.get(eid);
|
|
143
|
+
if (ai !== void 0) {
|
|
144
|
+
const edge = graph.edges[ai];
|
|
145
|
+
result.push({
|
|
146
|
+
neighborId: edge.targetId,
|
|
147
|
+
edge
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
for (const eid of idx.inEdges.get(nodeId) ?? []) {
|
|
152
|
+
const ai = idx.edgeById.get(eid);
|
|
153
|
+
if (ai !== void 0) {
|
|
154
|
+
const edge = graph.edges[ai];
|
|
155
|
+
result.push({
|
|
156
|
+
neighborId: edge.sourceId,
|
|
157
|
+
edge
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
//#endregion
|
|
165
|
+
//#region src/algorithms/csr.ts
|
|
166
|
+
const csrCache = /* @__PURE__ */ new WeakMap();
|
|
167
|
+
/** Get or lazily (re)build the CSR snapshot for a graph. */
|
|
168
|
+
function getCSR(graph) {
|
|
169
|
+
const idx = getIndex(graph);
|
|
170
|
+
const cached = csrCache.get(idx);
|
|
171
|
+
if (cached && cached.version === idx.version && cached.mode === graph.mode) return cached.csr;
|
|
172
|
+
const csr = buildCSR(graph);
|
|
173
|
+
csrCache.set(idx, {
|
|
174
|
+
version: idx.version,
|
|
175
|
+
mode: graph.mode,
|
|
176
|
+
csr
|
|
177
|
+
});
|
|
178
|
+
return csr;
|
|
179
|
+
}
|
|
180
|
+
function buildCSR(graph) {
|
|
181
|
+
const n = graph.nodes.length;
|
|
182
|
+
const m = graph.edges.length;
|
|
183
|
+
const ids = new Array(n);
|
|
184
|
+
const indexOf = /* @__PURE__ */ new Map();
|
|
185
|
+
for (let i = 0; i < n; i++) {
|
|
186
|
+
ids[i] = graph.nodes[i].id;
|
|
187
|
+
indexOf.set(ids[i], i);
|
|
188
|
+
}
|
|
189
|
+
const srcPos = new Int32Array(m);
|
|
190
|
+
const tgtPos = new Int32Array(m);
|
|
191
|
+
const nonDirected = new Uint8Array(m);
|
|
192
|
+
const outCounts = new Int32Array(n);
|
|
193
|
+
const inCounts = new Int32Array(n);
|
|
194
|
+
let firstNegativeEdge = -1;
|
|
195
|
+
for (let e = 0; e < m; e++) {
|
|
196
|
+
const edge = graph.edges[e];
|
|
197
|
+
if (firstNegativeEdge === -1 && (edge.weight ?? 1) < 0) firstNegativeEdge = e;
|
|
198
|
+
const s = indexOf.get(edge.sourceId);
|
|
199
|
+
const t = indexOf.get(edge.targetId);
|
|
200
|
+
if (s === void 0 || t === void 0) {
|
|
201
|
+
srcPos[e] = -1;
|
|
202
|
+
tgtPos[e] = -1;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
srcPos[e] = s;
|
|
206
|
+
tgtPos[e] = t;
|
|
207
|
+
const nd = getEdgeMode(graph, edge) !== "directed" ? 1 : 0;
|
|
208
|
+
nonDirected[e] = nd;
|
|
209
|
+
outCounts[s]++;
|
|
210
|
+
inCounts[t]++;
|
|
211
|
+
if (nd) {
|
|
212
|
+
outCounts[t]++;
|
|
213
|
+
inCounts[s]++;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const outOffsets = new Int32Array(n + 1);
|
|
217
|
+
const inOffsets = new Int32Array(n + 1);
|
|
218
|
+
for (let i = 0; i < n; i++) {
|
|
219
|
+
outOffsets[i + 1] = outOffsets[i] + outCounts[i];
|
|
220
|
+
inOffsets[i + 1] = inOffsets[i] + inCounts[i];
|
|
221
|
+
}
|
|
222
|
+
const outTargets = new Int32Array(outOffsets[n]);
|
|
223
|
+
const outEdgeIndex = new Int32Array(outOffsets[n]);
|
|
224
|
+
const inOrigins = new Int32Array(inOffsets[n]);
|
|
225
|
+
const inEdgeIndex = new Int32Array(inOffsets[n]);
|
|
226
|
+
const outCursor = outOffsets.slice(0, n);
|
|
227
|
+
const inCursor = inOffsets.slice(0, n);
|
|
228
|
+
for (let e = 0; e < m; e++) {
|
|
229
|
+
const s = srcPos[e];
|
|
230
|
+
const t = tgtPos[e];
|
|
231
|
+
if (s < 0) continue;
|
|
232
|
+
outTargets[outCursor[s]] = t;
|
|
233
|
+
outEdgeIndex[outCursor[s]++] = e;
|
|
234
|
+
inOrigins[inCursor[t]] = s;
|
|
235
|
+
inEdgeIndex[inCursor[t]++] = e;
|
|
236
|
+
if (nonDirected[e]) {
|
|
237
|
+
outTargets[outCursor[t]] = s;
|
|
238
|
+
outEdgeIndex[outCursor[t]++] = e;
|
|
239
|
+
inOrigins[inCursor[s]] = t;
|
|
240
|
+
inEdgeIndex[inCursor[s]++] = e;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
ids,
|
|
245
|
+
indexOf,
|
|
246
|
+
outOffsets,
|
|
247
|
+
outTargets,
|
|
248
|
+
outEdgeIndex,
|
|
249
|
+
inOffsets,
|
|
250
|
+
inOrigins,
|
|
251
|
+
inEdgeIndex,
|
|
252
|
+
firstNegativeEdge
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
//#endregion
|
|
257
|
+
//#region src/algorithms/paths.ts
|
|
258
|
+
/**
|
|
259
|
+
* Flat binary min-heap of `(distance, node position)` entries in parallel
|
|
260
|
+
* typed arrays. The Dijkstra/A* hot loops push one entry per relaxation, so
|
|
261
|
+
* avoiding a `{ pos, dist }` wrapper object per push (allocation + property
|
|
262
|
+
* loads in the sift comparisons) is a measurable win on 10k+ node graphs.
|
|
263
|
+
* Sifts move "holes" instead of swapping, halving array writes.
|
|
264
|
+
*/
|
|
265
|
+
var TypedMinHeap = class {
|
|
266
|
+
keys;
|
|
267
|
+
vals;
|
|
268
|
+
size = 0;
|
|
269
|
+
constructor(capacity) {
|
|
270
|
+
const cap = Math.max(capacity, 16);
|
|
271
|
+
this.keys = new Float64Array(cap);
|
|
272
|
+
this.vals = new Int32Array(cap);
|
|
273
|
+
}
|
|
274
|
+
push(key, val) {
|
|
275
|
+
if (this.size === this.keys.length) {
|
|
276
|
+
const keys$1 = new Float64Array(this.keys.length * 2);
|
|
277
|
+
const vals$1 = new Int32Array(this.vals.length * 2);
|
|
278
|
+
keys$1.set(this.keys);
|
|
279
|
+
vals$1.set(this.vals);
|
|
280
|
+
this.keys = keys$1;
|
|
281
|
+
this.vals = vals$1;
|
|
282
|
+
}
|
|
283
|
+
const { keys, vals } = this;
|
|
284
|
+
let hole = this.size++;
|
|
285
|
+
while (hole > 0) {
|
|
286
|
+
const parent = hole - 1 >> 1;
|
|
287
|
+
if (keys[parent] <= key) break;
|
|
288
|
+
keys[hole] = keys[parent];
|
|
289
|
+
vals[hole] = vals[parent];
|
|
290
|
+
hole = parent;
|
|
291
|
+
}
|
|
292
|
+
keys[hole] = key;
|
|
293
|
+
vals[hole] = val;
|
|
294
|
+
}
|
|
295
|
+
/** Key of the minimum entry; garbage when empty (check `size` first). */
|
|
296
|
+
peekKey() {
|
|
297
|
+
return this.keys[0];
|
|
298
|
+
}
|
|
299
|
+
/** Value of the minimum entry; garbage when empty (check `size` first). */
|
|
300
|
+
peekVal() {
|
|
301
|
+
return this.vals[0];
|
|
302
|
+
}
|
|
303
|
+
/** Remove the minimum entry (no-op shape: read via peek* first). */
|
|
304
|
+
pop() {
|
|
305
|
+
const { keys, vals } = this;
|
|
306
|
+
const last = --this.size;
|
|
307
|
+
if (last === 0) return;
|
|
308
|
+
const key = keys[last];
|
|
309
|
+
const val = vals[last];
|
|
310
|
+
let hole = 0;
|
|
311
|
+
for (;;) {
|
|
312
|
+
let child = hole * 2 + 1;
|
|
313
|
+
if (child >= last) break;
|
|
314
|
+
const right = child + 1;
|
|
315
|
+
if (right < last && keys[right] < keys[child]) child = right;
|
|
316
|
+
if (keys[child] >= key) break;
|
|
317
|
+
keys[hole] = keys[child];
|
|
318
|
+
vals[hole] = vals[child];
|
|
319
|
+
hole = child;
|
|
320
|
+
}
|
|
321
|
+
keys[hole] = key;
|
|
322
|
+
vals[hole] = val;
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
function computeShortestDistances(graph, sourceId, getWeight, algorithm, stopAtId) {
|
|
326
|
+
if (algorithm === "bellman-ford") return bellmanFordTyped(graph, sourceId, getWeight);
|
|
327
|
+
const csr = getCSR(graph);
|
|
328
|
+
const n = csr.ids.length;
|
|
329
|
+
const source = csr.indexOf.get(sourceId);
|
|
330
|
+
if (source === void 0) return {
|
|
331
|
+
source: -1,
|
|
332
|
+
distArr: new Float64Array(0),
|
|
333
|
+
prevArr: [],
|
|
334
|
+
stopDistance: Infinity
|
|
335
|
+
};
|
|
336
|
+
const distArr = new Float64Array(n).fill(Infinity);
|
|
337
|
+
const prevArr = new Array(n);
|
|
338
|
+
distArr[source] = 0;
|
|
339
|
+
prevArr[source] = [];
|
|
340
|
+
const stopAt = stopAtId !== void 0 ? csr.indexOf.get(stopAtId) : void 0;
|
|
341
|
+
let stopDistance = Infinity;
|
|
342
|
+
if (stopAt !== void 0) assertNoNegativeWeights(graph, csr, getWeight, "Dijkstra", "Use { algorithm: 'bellman-ford' } instead.");
|
|
343
|
+
if (!getWeight && !graph.edges.some((edge) => edge.weight !== void 0)) {
|
|
344
|
+
const queue = new Int32Array(n);
|
|
345
|
+
queue[0] = source;
|
|
346
|
+
let head = 0;
|
|
347
|
+
let tail = 1;
|
|
348
|
+
while (head < tail) {
|
|
349
|
+
const u = queue[head++];
|
|
350
|
+
if (distArr[u] > stopDistance) break;
|
|
351
|
+
if (u === stopAt) stopDistance = distArr[u];
|
|
352
|
+
const nextDistance = distArr[u] + 1;
|
|
353
|
+
for (let a = csr.outOffsets[u]; a < csr.outOffsets[u + 1]; a++) {
|
|
354
|
+
const v = csr.outTargets[a];
|
|
355
|
+
if (distArr[v] === Infinity) {
|
|
356
|
+
distArr[v] = nextDistance;
|
|
357
|
+
prevArr[v] = [u, csr.outEdgeIndex[a]];
|
|
358
|
+
queue[tail++] = v;
|
|
359
|
+
} else if (distArr[v] === nextDistance) prevArr[v].push(u, csr.outEdgeIndex[a]);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
} else {
|
|
363
|
+
const effectiveWeight = getWeight ?? ((edge) => edge.weight ?? 1);
|
|
364
|
+
const visited = new Uint8Array(n);
|
|
365
|
+
const pq = new TypedMinHeap(n);
|
|
366
|
+
pq.push(0, source);
|
|
367
|
+
while (pq.size > 0) {
|
|
368
|
+
const distance = pq.peekKey();
|
|
369
|
+
const u = pq.peekVal();
|
|
370
|
+
pq.pop();
|
|
371
|
+
if (visited[u] || distance !== distArr[u]) continue;
|
|
372
|
+
if (distance > stopDistance) break;
|
|
373
|
+
if (u === stopAt) stopDistance = distance;
|
|
374
|
+
visited[u] = 1;
|
|
375
|
+
for (let a = csr.outOffsets[u]; a < csr.outOffsets[u + 1]; a++) {
|
|
376
|
+
const edge = graph.edges[csr.outEdgeIndex[a]];
|
|
377
|
+
const weight = effectiveWeight(edge);
|
|
378
|
+
if (weight < 0) throw new Error(`Negative edge weight ${weight} on edge "${edge.sourceId}->${edge.targetId}" (id "${edge.id}"): Dijkstra requires non-negative weights. Use { algorithm: 'bellman-ford' } instead.`);
|
|
379
|
+
const v = csr.outTargets[a];
|
|
380
|
+
const nextDistance = distance + weight;
|
|
381
|
+
if (nextDistance < distArr[v]) {
|
|
382
|
+
distArr[v] = nextDistance;
|
|
383
|
+
prevArr[v] = [u, csr.outEdgeIndex[a]];
|
|
384
|
+
pq.push(nextDistance, v);
|
|
385
|
+
} else if (nextDistance === distArr[v] && distArr[v] !== Infinity) prevArr[v].push(u, csr.outEdgeIndex[a]);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return {
|
|
390
|
+
source,
|
|
391
|
+
distArr,
|
|
392
|
+
prevArr,
|
|
393
|
+
stopDistance
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Bellman-Ford adapted to the typed-array result shape. The O(VE) relaxation
|
|
398
|
+
* dominates, so the id→position conversion here is noise — and it keeps a
|
|
399
|
+
* single reconstruction path for both algorithms.
|
|
400
|
+
*/
|
|
401
|
+
function bellmanFordTyped(graph, sourceId, getWeight) {
|
|
402
|
+
const { dist, prev } = bellmanFord(graph, sourceId, getWeight);
|
|
403
|
+
const csr = getCSR(graph);
|
|
404
|
+
const idx = getIndex(graph);
|
|
405
|
+
const n = csr.ids.length;
|
|
406
|
+
const distArr = new Float64Array(n).fill(Infinity);
|
|
407
|
+
const prevArr = new Array(n);
|
|
408
|
+
for (const [id, distance] of dist) {
|
|
409
|
+
const pos = csr.indexOf.get(id);
|
|
410
|
+
if (pos === void 0) continue;
|
|
411
|
+
distArr[pos] = distance;
|
|
412
|
+
const pairs = [];
|
|
413
|
+
for (const { from, edge } of prev.get(id) ?? []) {
|
|
414
|
+
const fromPos = csr.indexOf.get(from);
|
|
415
|
+
const edgeIndex = idx.edgeById.get(edge.id);
|
|
416
|
+
if (fromPos === void 0 || edgeIndex === void 0) continue;
|
|
417
|
+
pairs.push(fromPos, edgeIndex);
|
|
418
|
+
}
|
|
419
|
+
prevArr[pos] = pairs;
|
|
420
|
+
}
|
|
421
|
+
return {
|
|
422
|
+
source: csr.indexOf.get(sourceId) ?? -1,
|
|
423
|
+
distArr,
|
|
424
|
+
prevArr,
|
|
425
|
+
stopDistance: Infinity
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
function bellmanFord(graph, sourceId, getWeight) {
|
|
429
|
+
const dist = /* @__PURE__ */ new Map();
|
|
430
|
+
const prev = /* @__PURE__ */ new Map();
|
|
431
|
+
const effectiveWeight = getWeight ?? ((edge) => edge.weight ?? 1);
|
|
432
|
+
for (const node of graph.nodes) {
|
|
433
|
+
dist.set(node.id, Infinity);
|
|
434
|
+
prev.set(node.id, []);
|
|
435
|
+
}
|
|
436
|
+
dist.set(sourceId, 0);
|
|
437
|
+
const directedEdges = [];
|
|
438
|
+
for (const edge of graph.edges) {
|
|
439
|
+
directedEdges.push({
|
|
440
|
+
fromId: edge.sourceId,
|
|
441
|
+
toId: edge.targetId,
|
|
442
|
+
edge
|
|
443
|
+
});
|
|
444
|
+
if (getEdgeMode(graph, edge) !== "directed") directedEdges.push({
|
|
445
|
+
fromId: edge.targetId,
|
|
446
|
+
toId: edge.sourceId,
|
|
447
|
+
edge
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
for (let i = 0; i < graph.nodes.length - 1; i++) {
|
|
451
|
+
let changed = false;
|
|
452
|
+
for (const { fromId, toId, edge } of directedEdges) {
|
|
453
|
+
const distance = dist.get(fromId);
|
|
454
|
+
if (distance === Infinity) continue;
|
|
455
|
+
const nextDistance = distance + effectiveWeight(edge);
|
|
456
|
+
const existing = dist.get(toId);
|
|
457
|
+
if (nextDistance < existing) {
|
|
458
|
+
dist.set(toId, nextDistance);
|
|
459
|
+
prev.set(toId, [{
|
|
460
|
+
from: fromId,
|
|
461
|
+
edge
|
|
462
|
+
}]);
|
|
463
|
+
changed = true;
|
|
464
|
+
} else if (nextDistance === existing && existing !== Infinity) {
|
|
465
|
+
const predecessors = prev.get(toId);
|
|
466
|
+
if (!predecessors.some((entry) => entry.from === fromId && entry.edge === edge)) predecessors.push({
|
|
467
|
+
from: fromId,
|
|
468
|
+
edge
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (!changed) break;
|
|
473
|
+
}
|
|
474
|
+
for (const { fromId, toId, edge } of directedEdges) {
|
|
475
|
+
const distance = dist.get(fromId);
|
|
476
|
+
if (distance === Infinity) continue;
|
|
477
|
+
if (distance + effectiveWeight(edge) < dist.get(toId)) throw new Error("Graph contains a negative-weight cycle reachable from the source node");
|
|
478
|
+
}
|
|
479
|
+
for (const [id, distance] of dist) if (distance === Infinity) {
|
|
480
|
+
dist.delete(id);
|
|
481
|
+
prev.delete(id);
|
|
482
|
+
}
|
|
483
|
+
return {
|
|
484
|
+
dist,
|
|
485
|
+
prev
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
function* reconstructPathsAt(graph, prevArr, sourceNode, sourcePos, targetPos, onPath = /* @__PURE__ */ new Set()) {
|
|
489
|
+
if (targetPos === sourcePos) {
|
|
490
|
+
yield {
|
|
491
|
+
source: sourceNode,
|
|
492
|
+
steps: []
|
|
493
|
+
};
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
const pairs = prevArr[targetPos];
|
|
497
|
+
if (!pairs || pairs.length === 0) return;
|
|
498
|
+
const targetNode = graph.nodes[targetPos];
|
|
499
|
+
onPath.add(targetPos);
|
|
500
|
+
for (let k = 0; k < pairs.length; k += 2) {
|
|
501
|
+
const fromPos = pairs[k];
|
|
502
|
+
if (onPath.has(fromPos)) continue;
|
|
503
|
+
const edge = graph.edges[pairs[k + 1]];
|
|
504
|
+
for (const prefix of reconstructPathsAt(graph, prevArr, sourceNode, sourcePos, fromPos, onPath)) yield {
|
|
505
|
+
source: sourceNode,
|
|
506
|
+
steps: [...prefix.steps, {
|
|
507
|
+
edge,
|
|
508
|
+
node: targetNode
|
|
509
|
+
}]
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
onPath.delete(targetPos);
|
|
513
|
+
}
|
|
514
|
+
function* genShortestPaths(graph, opts) {
|
|
515
|
+
const sourceId = resolveFrom(graph, opts);
|
|
516
|
+
const { source, distArr, prevArr, stopDistance } = computeShortestDistances(graph, sourceId, opts?.getWeight, opts?.algorithm, opts?.to);
|
|
517
|
+
const sourceNode = source !== -1 ? graph.nodes[source] : graph.nodes.find((node) => node.id === sourceId);
|
|
518
|
+
if (source === -1) {
|
|
519
|
+
if (opts?.to === sourceId) yield {
|
|
520
|
+
source: sourceNode,
|
|
521
|
+
steps: []
|
|
522
|
+
};
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
const csr = getCSR(graph);
|
|
526
|
+
if (opts?.to) {
|
|
527
|
+
const target = csr.indexOf.get(opts.to);
|
|
528
|
+
if (target === void 0 || distArr[target] === Infinity || distArr[target] > stopDistance) return;
|
|
529
|
+
yield* reconstructPathsAt(graph, prevArr, sourceNode, source, target);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
for (let target = 0; target < distArr.length; target++) {
|
|
533
|
+
if (target === source || distArr[target] === Infinity || distArr[target] > stopDistance) continue;
|
|
534
|
+
yield* reconstructPathsAt(graph, prevArr, sourceNode, source, target);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
function getShortestPaths(graph, opts) {
|
|
538
|
+
return [...genShortestPaths(graph, opts)];
|
|
539
|
+
}
|
|
540
|
+
function getShortestPath(graph, opts) {
|
|
541
|
+
if (opts.algorithm !== "bellman-ford") return bidirectionalShortestPath(graph, resolveFrom(graph, opts), opts.to, opts.getWeight);
|
|
542
|
+
for (const path of genShortestPaths(graph, opts)) return path;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Sublinear searches (early-exit, bidirectional) may legitimately terminate
|
|
546
|
+
* without ever scanning a negative edge, so the throw-on-negative contract
|
|
547
|
+
* must be enforced up front: O(1) via the CSR's cached flag for the default
|
|
548
|
+
* weight, or one O(edges) sweep for a custom `getWeight`.
|
|
549
|
+
*/
|
|
550
|
+
function assertNoNegativeWeights(graph, csr, getWeight, algorithmName, remedy) {
|
|
551
|
+
let offending;
|
|
552
|
+
let weight = 0;
|
|
553
|
+
if (getWeight === void 0) {
|
|
554
|
+
if (csr.firstNegativeEdge !== -1) {
|
|
555
|
+
offending = graph.edges[csr.firstNegativeEdge];
|
|
556
|
+
weight = offending.weight ?? 1;
|
|
557
|
+
}
|
|
558
|
+
} else for (const edge of graph.edges) {
|
|
559
|
+
const w = getWeight(edge);
|
|
560
|
+
if (w < 0) {
|
|
561
|
+
offending = edge;
|
|
562
|
+
weight = w;
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
if (offending) throw new Error(`Negative edge weight ${weight} on edge "${offending.sourceId}->${offending.targetId}" (id "${offending.id}"): ${algorithmName} requires non-negative weights. ${remedy}`);
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Bidirectional Dijkstra for a single source→target query. Forward search
|
|
570
|
+
* runs on the traversable arcs, backward search on the reverse arcs; `mu`
|
|
571
|
+
* tracks the best meeting cost and the search stops when the two frontiers
|
|
572
|
+
* prove no better meeting exists (Pohl's `topF + topB >= mu` condition).
|
|
573
|
+
* Returns one shortest path (ties broken arbitrarily, as before).
|
|
574
|
+
*/
|
|
575
|
+
function bidirectionalShortestPath(graph, sourceId, targetId, getWeight) {
|
|
576
|
+
const csr = getCSR(graph);
|
|
577
|
+
const source = csr.indexOf.get(sourceId);
|
|
578
|
+
const target = csr.indexOf.get(targetId);
|
|
579
|
+
if (source === void 0 || target === void 0) return void 0;
|
|
580
|
+
const sourceNode = graph.nodes[source];
|
|
581
|
+
if (source === target) return {
|
|
582
|
+
source: sourceNode,
|
|
583
|
+
steps: []
|
|
584
|
+
};
|
|
585
|
+
assertNoNegativeWeights(graph, csr, getWeight, "Dijkstra", "Use { algorithm: 'bellman-ford' } instead.");
|
|
586
|
+
const effectiveWeight = getWeight ?? ((edge) => edge.weight ?? 1);
|
|
587
|
+
const n = csr.ids.length;
|
|
588
|
+
const distF = new Float64Array(n).fill(Infinity);
|
|
589
|
+
const distB = new Float64Array(n).fill(Infinity);
|
|
590
|
+
const predF = new Int32Array(n).fill(-1);
|
|
591
|
+
const predFEdge = new Int32Array(n).fill(-1);
|
|
592
|
+
const predB = new Int32Array(n).fill(-1);
|
|
593
|
+
const predBEdge = new Int32Array(n).fill(-1);
|
|
594
|
+
const settledF = new Uint8Array(n);
|
|
595
|
+
const settledB = new Uint8Array(n);
|
|
596
|
+
const pqF = new TypedMinHeap(n);
|
|
597
|
+
const pqB = new TypedMinHeap(n);
|
|
598
|
+
distF[source] = 0;
|
|
599
|
+
distB[target] = 0;
|
|
600
|
+
pqF.push(0, source);
|
|
601
|
+
pqB.push(0, target);
|
|
602
|
+
let mu = Infinity;
|
|
603
|
+
let meet = -1;
|
|
604
|
+
/** Discard stale/settled heap entries; return the next valid key. */
|
|
605
|
+
const validTop = (pq, dist, settled) => {
|
|
606
|
+
while (pq.size > 0) {
|
|
607
|
+
const key = pq.peekKey();
|
|
608
|
+
const pos = pq.peekVal();
|
|
609
|
+
if (settled[pos] || key !== dist[pos]) {
|
|
610
|
+
pq.pop();
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
return key;
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
const scanForward = () => {
|
|
617
|
+
const d = pqF.peekKey();
|
|
618
|
+
const u = pqF.peekVal();
|
|
619
|
+
pqF.pop();
|
|
620
|
+
settledF[u] = 1;
|
|
621
|
+
for (let a = csr.outOffsets[u]; a < csr.outOffsets[u + 1]; a++) {
|
|
622
|
+
const edge = graph.edges[csr.outEdgeIndex[a]];
|
|
623
|
+
const weight = effectiveWeight(edge);
|
|
624
|
+
const v = csr.outTargets[a];
|
|
625
|
+
const next = d + weight;
|
|
626
|
+
if (next < distF[v]) {
|
|
627
|
+
distF[v] = next;
|
|
628
|
+
predF[v] = u;
|
|
629
|
+
predFEdge[v] = csr.outEdgeIndex[a];
|
|
630
|
+
pqF.push(next, v);
|
|
631
|
+
}
|
|
632
|
+
if (distB[v] !== Infinity && next + distB[v] < mu) {
|
|
633
|
+
mu = next + distB[v];
|
|
634
|
+
meet = v;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
const scanBackward = () => {
|
|
639
|
+
const d = pqB.peekKey();
|
|
640
|
+
const u = pqB.peekVal();
|
|
641
|
+
pqB.pop();
|
|
642
|
+
settledB[u] = 1;
|
|
643
|
+
for (let a = csr.inOffsets[u]; a < csr.inOffsets[u + 1]; a++) {
|
|
644
|
+
const edge = graph.edges[csr.inEdgeIndex[a]];
|
|
645
|
+
const weight = effectiveWeight(edge);
|
|
646
|
+
const v = csr.inOrigins[a];
|
|
647
|
+
const next = d + weight;
|
|
648
|
+
if (next < distB[v]) {
|
|
649
|
+
distB[v] = next;
|
|
650
|
+
predB[v] = u;
|
|
651
|
+
predBEdge[v] = csr.inEdgeIndex[a];
|
|
652
|
+
pqB.push(next, v);
|
|
653
|
+
}
|
|
654
|
+
if (distF[v] !== Infinity && next + distF[v] < mu) {
|
|
655
|
+
mu = next + distF[v];
|
|
656
|
+
meet = v;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
for (;;) {
|
|
661
|
+
const topF = validTop(pqF, distF, settledF);
|
|
662
|
+
const topB = validTop(pqB, distB, settledB);
|
|
663
|
+
if (topF === void 0 || topB === void 0) break;
|
|
664
|
+
if (topF + topB >= mu) break;
|
|
665
|
+
if (topF <= topB) scanForward();
|
|
666
|
+
else scanBackward();
|
|
667
|
+
}
|
|
668
|
+
if (meet === -1) return void 0;
|
|
669
|
+
const steps = [];
|
|
670
|
+
for (let v = meet; v !== source; v = predF[v]) steps.unshift({
|
|
671
|
+
edge: graph.edges[predFEdge[v]],
|
|
672
|
+
node: graph.nodes[v]
|
|
673
|
+
});
|
|
674
|
+
for (let v = meet; v !== target;) {
|
|
675
|
+
const nextNode = predB[v];
|
|
676
|
+
steps.push({
|
|
677
|
+
edge: graph.edges[predBEdge[v]],
|
|
678
|
+
node: graph.nodes[nextNode]
|
|
679
|
+
});
|
|
680
|
+
v = nextNode;
|
|
681
|
+
}
|
|
682
|
+
return {
|
|
683
|
+
source: sourceNode,
|
|
684
|
+
steps
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
function getSimplePaths(graph, opts) {
|
|
688
|
+
return [...genSimplePaths(graph, opts)];
|
|
689
|
+
}
|
|
690
|
+
function* genSimplePaths(graph, opts) {
|
|
691
|
+
const idx = getIndex(graph);
|
|
692
|
+
const sourceId = resolveFrom(graph, opts);
|
|
693
|
+
const sourceNi = idx.nodeById.get(sourceId);
|
|
694
|
+
const sourceNode = sourceNi !== void 0 ? graph.nodes[sourceNi] : graph.nodes.find((node) => node.id === sourceId);
|
|
695
|
+
const targetId = opts?.to;
|
|
696
|
+
const visited = /* @__PURE__ */ new Set();
|
|
697
|
+
const currentSteps = [];
|
|
698
|
+
function* dfsCollect(nodeId) {
|
|
699
|
+
visited.add(nodeId);
|
|
700
|
+
if (targetId !== void 0) {
|
|
701
|
+
if (nodeId === targetId) {
|
|
702
|
+
yield {
|
|
703
|
+
source: sourceNode,
|
|
704
|
+
steps: [...currentSteps]
|
|
705
|
+
};
|
|
706
|
+
visited.delete(nodeId);
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
} else if (currentSteps.length > 0) yield {
|
|
710
|
+
source: sourceNode,
|
|
711
|
+
steps: [...currentSteps]
|
|
712
|
+
};
|
|
713
|
+
for (const { neighborId, edge } of getNeighborEdges(graph, nodeId)) if (!visited.has(neighborId)) {
|
|
714
|
+
const neighborNi = idx.nodeById.get(neighborId);
|
|
715
|
+
const neighborNode = neighborNi !== void 0 ? graph.nodes[neighborNi] : graph.nodes.find((node) => node.id === neighborId);
|
|
716
|
+
currentSteps.push({
|
|
717
|
+
edge,
|
|
718
|
+
node: neighborNode
|
|
719
|
+
});
|
|
720
|
+
yield* dfsCollect(neighborId);
|
|
721
|
+
currentSteps.pop();
|
|
722
|
+
}
|
|
723
|
+
visited.delete(nodeId);
|
|
724
|
+
}
|
|
725
|
+
yield* dfsCollect(sourceId);
|
|
726
|
+
}
|
|
727
|
+
function getSimplePath(graph, opts) {
|
|
728
|
+
for (const path of genSimplePaths(graph, opts)) return path;
|
|
729
|
+
}
|
|
730
|
+
function getStronglyConnectedComponents(graph) {
|
|
731
|
+
const idx = getIndex(graph);
|
|
732
|
+
let indexCounter = 0;
|
|
733
|
+
const nodeIndex = /* @__PURE__ */ new Map();
|
|
734
|
+
const lowlink = /* @__PURE__ */ new Map();
|
|
735
|
+
const onStack = /* @__PURE__ */ new Set();
|
|
736
|
+
const stack = [];
|
|
737
|
+
const result = [];
|
|
738
|
+
function strongconnect(id) {
|
|
739
|
+
nodeIndex.set(id, indexCounter);
|
|
740
|
+
lowlink.set(id, indexCounter);
|
|
741
|
+
indexCounter++;
|
|
742
|
+
stack.push(id);
|
|
743
|
+
onStack.add(id);
|
|
744
|
+
for (const neighborId of getNeighborIds(graph, id)) if (!nodeIndex.has(neighborId)) {
|
|
745
|
+
strongconnect(neighborId);
|
|
746
|
+
lowlink.set(id, Math.min(lowlink.get(id), lowlink.get(neighborId)));
|
|
747
|
+
} else if (onStack.has(neighborId)) lowlink.set(id, Math.min(lowlink.get(id), nodeIndex.get(neighborId)));
|
|
748
|
+
if (lowlink.get(id) === nodeIndex.get(id)) {
|
|
749
|
+
const component = [];
|
|
750
|
+
let neighborId;
|
|
751
|
+
do {
|
|
752
|
+
neighborId = stack.pop();
|
|
753
|
+
onStack.delete(neighborId);
|
|
754
|
+
const ni = idx.nodeById.get(neighborId);
|
|
755
|
+
if (ni !== void 0) component.push(graph.nodes[ni]);
|
|
756
|
+
} while (neighborId !== id);
|
|
757
|
+
result.push(component);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
for (const node of graph.nodes) if (!nodeIndex.has(node.id)) strongconnect(node.id);
|
|
761
|
+
return result;
|
|
762
|
+
}
|
|
763
|
+
function getCycles(graph) {
|
|
764
|
+
return [...genCycles(graph)];
|
|
765
|
+
}
|
|
766
|
+
function* genCycles(graph) {
|
|
767
|
+
const kind = getEffectiveModeKind(graph);
|
|
768
|
+
if (kind === "mixed") yield* genCyclesMixed(graph);
|
|
769
|
+
else if (kind === "non-directed") yield* genCyclesUndirected(graph);
|
|
770
|
+
else yield* genCyclesDirected(graph);
|
|
771
|
+
}
|
|
772
|
+
function* genCyclesDirected(graph) {
|
|
773
|
+
const idx = getIndex(graph);
|
|
774
|
+
const sortedIds = graph.nodes.map((node) => node.id).sort();
|
|
775
|
+
for (let startIndex = 0; startIndex < sortedIds.length; startIndex++) {
|
|
776
|
+
const startId = sortedIds[startIndex];
|
|
777
|
+
const allowed = new Set(sortedIds.slice(startIndex));
|
|
778
|
+
const visited = /* @__PURE__ */ new Set();
|
|
779
|
+
const steps = [];
|
|
780
|
+
const startNi = idx.nodeById.get(startId);
|
|
781
|
+
const startNode = graph.nodes[startNi];
|
|
782
|
+
const found = [];
|
|
783
|
+
function dfsFind(currentId) {
|
|
784
|
+
visited.add(currentId);
|
|
785
|
+
for (const eid of idx.outEdges.get(currentId) ?? []) {
|
|
786
|
+
const ai = idx.edgeById.get(eid);
|
|
787
|
+
if (ai === void 0) continue;
|
|
788
|
+
const edge = graph.edges[ai];
|
|
789
|
+
const neighborId = edge.targetId;
|
|
790
|
+
if (neighborId === startId && (steps.length > 0 || currentId === startId)) found.push({
|
|
791
|
+
source: startNode,
|
|
792
|
+
steps: [...steps, {
|
|
793
|
+
edge,
|
|
794
|
+
node: startNode
|
|
795
|
+
}]
|
|
796
|
+
});
|
|
797
|
+
else if (allowed.has(neighborId) && !visited.has(neighborId)) {
|
|
798
|
+
const ni = idx.nodeById.get(neighborId);
|
|
799
|
+
steps.push({
|
|
800
|
+
edge,
|
|
801
|
+
node: graph.nodes[ni]
|
|
802
|
+
});
|
|
803
|
+
dfsFind(neighborId);
|
|
804
|
+
steps.pop();
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
visited.delete(currentId);
|
|
808
|
+
}
|
|
809
|
+
dfsFind(startId);
|
|
810
|
+
yield* found;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
function* genCyclesUndirected(graph) {
|
|
814
|
+
const idx = getIndex(graph);
|
|
815
|
+
const sortedIds = graph.nodes.map((node) => node.id).sort();
|
|
816
|
+
const seen = /* @__PURE__ */ new Set();
|
|
817
|
+
for (let startIndex = 0; startIndex < sortedIds.length; startIndex++) {
|
|
818
|
+
const startId = sortedIds[startIndex];
|
|
819
|
+
const allowed = new Set(sortedIds.slice(startIndex));
|
|
820
|
+
const visited = /* @__PURE__ */ new Set();
|
|
821
|
+
const steps = [];
|
|
822
|
+
const startNi = idx.nodeById.get(startId);
|
|
823
|
+
const startNode = graph.nodes[startNi];
|
|
824
|
+
const found = [];
|
|
825
|
+
function dfsFind(currentId, arrivalEdgeId) {
|
|
826
|
+
visited.add(currentId);
|
|
827
|
+
for (const { neighborId, edge } of getNeighborEdgesAll(graph, currentId)) {
|
|
828
|
+
if (edge.id === arrivalEdgeId) continue;
|
|
829
|
+
if (neighborId === startId && (steps.length >= 1 || edge.sourceId === edge.targetId)) {
|
|
830
|
+
const cycleEdgeIds = [...steps.map((step) => step.edge.id), edge.id].sort().join(",");
|
|
831
|
+
if (!seen.has(cycleEdgeIds)) {
|
|
832
|
+
seen.add(cycleEdgeIds);
|
|
833
|
+
found.push({
|
|
834
|
+
source: startNode,
|
|
835
|
+
steps: [...steps, {
|
|
836
|
+
edge,
|
|
837
|
+
node: startNode
|
|
838
|
+
}]
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
} else if (allowed.has(neighborId) && !visited.has(neighborId)) {
|
|
842
|
+
const ni = idx.nodeById.get(neighborId);
|
|
843
|
+
steps.push({
|
|
844
|
+
edge,
|
|
845
|
+
node: graph.nodes[ni]
|
|
846
|
+
});
|
|
847
|
+
dfsFind(neighborId, edge.id);
|
|
848
|
+
steps.pop();
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
visited.delete(currentId);
|
|
852
|
+
}
|
|
853
|
+
dfsFind(startId, null);
|
|
854
|
+
yield* found;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* Exact simple-cycle enumeration for graphs mixing directed and non-directed
|
|
859
|
+
* edges. Traverses directed edges source→target only and non-directed edges
|
|
860
|
+
* both ways; a cycle may use each edge at most once, visits distinct nodes,
|
|
861
|
+
* and is identified by its set of traversed edge ids.
|
|
862
|
+
*/
|
|
863
|
+
function* genCyclesMixed(graph) {
|
|
864
|
+
const idx = getIndex(graph);
|
|
865
|
+
const sortedIds = graph.nodes.map((node) => node.id).sort();
|
|
866
|
+
const seen = /* @__PURE__ */ new Set();
|
|
867
|
+
for (let startIndex = 0; startIndex < sortedIds.length; startIndex++) {
|
|
868
|
+
const startId = sortedIds[startIndex];
|
|
869
|
+
const allowed = new Set(sortedIds.slice(startIndex));
|
|
870
|
+
const visited = /* @__PURE__ */ new Set();
|
|
871
|
+
const steps = [];
|
|
872
|
+
const pathEdgeIds = /* @__PURE__ */ new Set();
|
|
873
|
+
const startNi = idx.nodeById.get(startId);
|
|
874
|
+
const startNode = graph.nodes[startNi];
|
|
875
|
+
const found = [];
|
|
876
|
+
function dfsFind(currentId) {
|
|
877
|
+
visited.add(currentId);
|
|
878
|
+
for (const { neighborId, edge } of getNeighborEdges(graph, currentId)) {
|
|
879
|
+
if (pathEdgeIds.has(edge.id)) continue;
|
|
880
|
+
if (neighborId === startId && (steps.length >= 1 || edge.sourceId === edge.targetId)) {
|
|
881
|
+
const cycleEdgeIds = [...steps.map((step) => step.edge.id), edge.id].sort().join(",");
|
|
882
|
+
if (!seen.has(cycleEdgeIds)) {
|
|
883
|
+
seen.add(cycleEdgeIds);
|
|
884
|
+
found.push({
|
|
885
|
+
source: startNode,
|
|
886
|
+
steps: [...steps, {
|
|
887
|
+
edge,
|
|
888
|
+
node: startNode
|
|
889
|
+
}]
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
} else if (allowed.has(neighborId) && !visited.has(neighborId)) {
|
|
893
|
+
const ni = idx.nodeById.get(neighborId);
|
|
894
|
+
steps.push({
|
|
895
|
+
edge,
|
|
896
|
+
node: graph.nodes[ni]
|
|
897
|
+
});
|
|
898
|
+
pathEdgeIds.add(edge.id);
|
|
899
|
+
dfsFind(neighborId);
|
|
900
|
+
pathEdgeIds.delete(edge.id);
|
|
901
|
+
steps.pop();
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
visited.delete(currentId);
|
|
905
|
+
}
|
|
906
|
+
dfsFind(startId);
|
|
907
|
+
yield* found;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
function getAllPairsShortestPaths(graph, opts) {
|
|
911
|
+
const algorithm = opts?.algorithm ?? "dijkstra";
|
|
912
|
+
if (algorithm === "floyd-warshall") return floydWarshallAllPaths(graph, opts?.getWeight);
|
|
913
|
+
if (algorithm === "bellman-ford") return bellmanFordAllPaths(graph, opts?.getWeight);
|
|
914
|
+
return dijkstraAllPaths(graph, opts?.getWeight);
|
|
915
|
+
}
|
|
916
|
+
function bellmanFordAllPaths(graph, getWeight) {
|
|
917
|
+
const results = [];
|
|
918
|
+
for (const node of graph.nodes) results.push(...getShortestPaths(graph, {
|
|
919
|
+
from: node.id,
|
|
920
|
+
getWeight,
|
|
921
|
+
algorithm: "bellman-ford"
|
|
922
|
+
}));
|
|
923
|
+
return results;
|
|
924
|
+
}
|
|
925
|
+
function dijkstraAllPaths(graph, getWeight) {
|
|
926
|
+
const results = [];
|
|
927
|
+
for (const node of graph.nodes) results.push(...getShortestPaths(graph, {
|
|
928
|
+
from: node.id,
|
|
929
|
+
getWeight
|
|
930
|
+
}));
|
|
931
|
+
return results;
|
|
932
|
+
}
|
|
933
|
+
function floydWarshallAllPaths(graph, getWeight) {
|
|
934
|
+
const idx = getIndex(graph);
|
|
935
|
+
const weight = getWeight ?? ((edge) => edge.weight ?? 1);
|
|
936
|
+
const nodeIds = graph.nodes.map((node) => node.id);
|
|
937
|
+
const nodeCount = nodeIds.length;
|
|
938
|
+
const indexOf = /* @__PURE__ */ new Map();
|
|
939
|
+
for (let i = 0; i < nodeCount; i++) indexOf.set(nodeIds[i], i);
|
|
940
|
+
const INF = Infinity;
|
|
941
|
+
const dist = Array.from({ length: nodeCount }, () => Array(nodeCount).fill(INF));
|
|
942
|
+
const prev = Array.from({ length: nodeCount }, () => Array.from({ length: nodeCount }, () => []));
|
|
943
|
+
for (let i = 0; i < nodeCount; i++) dist[i][i] = 0;
|
|
944
|
+
for (const edge of graph.edges) {
|
|
945
|
+
const source = indexOf.get(edge.sourceId);
|
|
946
|
+
const target = indexOf.get(edge.targetId);
|
|
947
|
+
const edgeWeight = weight(edge);
|
|
948
|
+
if (edgeWeight < dist[source][target]) {
|
|
949
|
+
dist[source][target] = edgeWeight;
|
|
950
|
+
prev[source][target] = [{
|
|
951
|
+
from: source,
|
|
952
|
+
edge
|
|
953
|
+
}];
|
|
954
|
+
} else if (edgeWeight === dist[source][target] && edgeWeight < INF) prev[source][target].push({
|
|
955
|
+
from: source,
|
|
956
|
+
edge
|
|
957
|
+
});
|
|
958
|
+
if (getEdgeMode(graph, edge) !== "directed") {
|
|
959
|
+
if (edgeWeight < dist[target][source]) {
|
|
960
|
+
dist[target][source] = edgeWeight;
|
|
961
|
+
prev[target][source] = [{
|
|
962
|
+
from: target,
|
|
963
|
+
edge
|
|
964
|
+
}];
|
|
965
|
+
} else if (edgeWeight === dist[target][source] && edgeWeight < INF) prev[target][source].push({
|
|
966
|
+
from: target,
|
|
967
|
+
edge
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
for (let k = 0; k < nodeCount; k++) for (let i = 0; i < nodeCount; i++) for (let j = 0; j < nodeCount; j++) {
|
|
972
|
+
if (dist[i][k] === INF || dist[k][j] === INF) continue;
|
|
973
|
+
const nextDistance = dist[i][k] + dist[k][j];
|
|
974
|
+
if (nextDistance < dist[i][j]) {
|
|
975
|
+
dist[i][j] = nextDistance;
|
|
976
|
+
prev[i][j] = prev[k][j].map((entry) => ({ ...entry }));
|
|
977
|
+
} else if (nextDistance === dist[i][j] && nextDistance < INF) {
|
|
978
|
+
for (const entry of prev[k][j]) if (!prev[i][j].some((existing) => existing.edge.id === entry.edge.id)) prev[i][j].push({ ...entry });
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
for (let i = 0; i < nodeCount; i++) if (dist[i][i] < 0) throw new Error(`Negative cycle detected through node "${nodeIds[i]}": all-pairs shortest paths are undefined. Remove the negative cycle, or use getShortestPaths with { algorithm: 'bellman-ford' } per source to locate it.`);
|
|
982
|
+
const results = [];
|
|
983
|
+
for (let i = 0; i < nodeCount; i++) {
|
|
984
|
+
const sourceNi = idx.nodeById.get(nodeIds[i]);
|
|
985
|
+
if (sourceNi === void 0) continue;
|
|
986
|
+
const sourceNode = graph.nodes[sourceNi];
|
|
987
|
+
for (let j = 0; j < nodeCount; j++) {
|
|
988
|
+
if (i === j || dist[i][j] === INF) continue;
|
|
989
|
+
results.push(...fwReconstruct(graph, prev, nodeIds, sourceNode, i, j));
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
return results;
|
|
993
|
+
}
|
|
994
|
+
function fwReconstruct(graph, prev, nodeIds, sourceNode, sourceIdx, targetIdx) {
|
|
995
|
+
if (sourceIdx === targetIdx) return [{
|
|
996
|
+
source: sourceNode,
|
|
997
|
+
steps: []
|
|
998
|
+
}];
|
|
999
|
+
const predecessors = prev[sourceIdx][targetIdx];
|
|
1000
|
+
if (predecessors.length === 0) return [];
|
|
1001
|
+
const targetNi = getIndex(graph).nodeById.get(nodeIds[targetIdx]);
|
|
1002
|
+
if (targetNi === void 0) return [];
|
|
1003
|
+
const targetNode = graph.nodes[targetNi];
|
|
1004
|
+
const results = [];
|
|
1005
|
+
for (const { from, edge } of predecessors) {
|
|
1006
|
+
const prefixPaths = fwReconstruct(graph, prev, nodeIds, sourceNode, sourceIdx, from);
|
|
1007
|
+
for (const prefix of prefixPaths) results.push({
|
|
1008
|
+
source: sourceNode,
|
|
1009
|
+
steps: [...prefix.steps, {
|
|
1010
|
+
edge,
|
|
1011
|
+
node: targetNode
|
|
1012
|
+
}]
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
return results;
|
|
1016
|
+
}
|
|
1017
|
+
function getAStarPath(graph, opts) {
|
|
1018
|
+
const idx = getIndex(graph);
|
|
1019
|
+
const { from: sourceId, to: targetId, heuristic } = opts;
|
|
1020
|
+
const getWeight = opts.getWeight ?? ((edge) => edge.weight ?? 1);
|
|
1021
|
+
const sourceNi = idx.nodeById.get(sourceId);
|
|
1022
|
+
if (sourceNi === void 0) return void 0;
|
|
1023
|
+
if (!idx.nodeById.has(targetId)) return void 0;
|
|
1024
|
+
if (sourceId === targetId) return {
|
|
1025
|
+
source: graph.nodes[sourceNi],
|
|
1026
|
+
steps: []
|
|
1027
|
+
};
|
|
1028
|
+
const csr = getCSR(graph);
|
|
1029
|
+
const n = csr.ids.length;
|
|
1030
|
+
const source = csr.indexOf.get(sourceId);
|
|
1031
|
+
const target = csr.indexOf.get(targetId);
|
|
1032
|
+
const gScore = new Float64Array(n).fill(Infinity);
|
|
1033
|
+
const cameFromPos = new Int32Array(n).fill(-1);
|
|
1034
|
+
const cameFromEdge = new Int32Array(n).fill(-1);
|
|
1035
|
+
const closed = new Uint8Array(n);
|
|
1036
|
+
const openSet = new TypedMinHeap(n);
|
|
1037
|
+
assertNoNegativeWeights(graph, csr, opts.getWeight, "A*", "Use getShortestPath with { algorithm: 'bellman-ford' } instead.");
|
|
1038
|
+
gScore[source] = 0;
|
|
1039
|
+
openSet.push(heuristic(sourceId), source);
|
|
1040
|
+
while (openSet.size > 0) {
|
|
1041
|
+
const current = openSet.peekVal();
|
|
1042
|
+
openSet.pop();
|
|
1043
|
+
if (closed[current]) continue;
|
|
1044
|
+
if (current === target) {
|
|
1045
|
+
const steps = [];
|
|
1046
|
+
let cursor = target;
|
|
1047
|
+
while (cursor !== source) {
|
|
1048
|
+
steps.unshift({
|
|
1049
|
+
edge: graph.edges[cameFromEdge[cursor]],
|
|
1050
|
+
node: graph.nodes[cursor]
|
|
1051
|
+
});
|
|
1052
|
+
cursor = cameFromPos[cursor];
|
|
1053
|
+
}
|
|
1054
|
+
return {
|
|
1055
|
+
source: graph.nodes[sourceNi],
|
|
1056
|
+
steps
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
closed[current] = 1;
|
|
1060
|
+
for (let a = csr.outOffsets[current]; a < csr.outOffsets[current + 1]; a++) {
|
|
1061
|
+
const edge = graph.edges[csr.outEdgeIndex[a]];
|
|
1062
|
+
const weight = getWeight(edge);
|
|
1063
|
+
const neighbor = csr.outTargets[a];
|
|
1064
|
+
if (closed[neighbor]) continue;
|
|
1065
|
+
const tentativeScore = gScore[current] + weight;
|
|
1066
|
+
if (tentativeScore < gScore[neighbor]) {
|
|
1067
|
+
cameFromPos[neighbor] = current;
|
|
1068
|
+
cameFromEdge[neighbor] = csr.outEdgeIndex[a];
|
|
1069
|
+
gScore[neighbor] = tentativeScore;
|
|
1070
|
+
openSet.push(tentativeScore + heuristic(csr.ids[neighbor]), neighbor);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
function getJoinedPath(headPath, tailPath) {
|
|
1076
|
+
const headEnd = headPath.steps.length > 0 ? headPath.steps[headPath.steps.length - 1].node : headPath.source;
|
|
1077
|
+
if (headEnd.id !== tailPath.source.id) throw new Error(`Paths cannot be joined: head path ends at "${headEnd.id}" but tail path starts at "${tailPath.source.id}"`);
|
|
1078
|
+
return {
|
|
1079
|
+
source: headPath.source,
|
|
1080
|
+
steps: [...headPath.steps, ...tailPath.steps]
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* @deprecated Use {@link getJoinedPath}.
|
|
1085
|
+
*/
|
|
1086
|
+
function joinPaths(headPath, tailPath) {
|
|
1087
|
+
return getJoinedPath(headPath, tailPath);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
//#endregion
|
|
1091
|
+
//#region src/transforms.ts
|
|
1092
|
+
/**
|
|
1093
|
+
* Flattens a hierarchical graph into a flat graph with only leaf nodes.
|
|
1094
|
+
*
|
|
1095
|
+
* - Edges targeting a compound node resolve to its initial child (recursively).
|
|
1096
|
+
* - Edges originating from a compound node expand to all leaf descendants.
|
|
1097
|
+
* - Only leaf nodes (nodes with no children) appear in the result.
|
|
1098
|
+
* - Duplicate edges (same source + target) are deduplicated.
|
|
1099
|
+
*
|
|
1100
|
+
* @example
|
|
1101
|
+
* ```ts
|
|
1102
|
+
* import { createGraph, getFlattenedGraph } from '@statelyai/graph';
|
|
1103
|
+
*
|
|
1104
|
+
* const graph = createGraph({
|
|
1105
|
+
* nodes: [
|
|
1106
|
+
* { id: 'parent', initialNodeId: 'child1' },
|
|
1107
|
+
* { id: 'child1', parentId: 'parent' },
|
|
1108
|
+
* { id: 'child2', parentId: 'parent' },
|
|
1109
|
+
* { id: 'other' },
|
|
1110
|
+
* ],
|
|
1111
|
+
* edges: [{ id: 'e1', sourceId: 'other', targetId: 'parent' }],
|
|
1112
|
+
* });
|
|
1113
|
+
*
|
|
1114
|
+
* const flat = getFlattenedGraph(graph);
|
|
1115
|
+
* // flat.nodes → [child1, child2, other] (leaf nodes only)
|
|
1116
|
+
* // flat.edges → edge from 'other' → 'child1' (resolved via initialNodeId)
|
|
1117
|
+
* ```
|
|
1118
|
+
*/
|
|
1119
|
+
function getFlattenedGraph(graph) {
|
|
1120
|
+
const idx = getIndex(graph);
|
|
1121
|
+
const leaves = /* @__PURE__ */ new Set();
|
|
1122
|
+
for (const node of graph.nodes) if ((idx.childNodes.get(node.id) ?? []).length === 0) leaves.add(node.id);
|
|
1123
|
+
function resolveInitial(nodeId, seen = /* @__PURE__ */ new Set()) {
|
|
1124
|
+
if (leaves.has(nodeId)) return nodeId;
|
|
1125
|
+
if (seen.has(nodeId)) return null;
|
|
1126
|
+
seen.add(nodeId);
|
|
1127
|
+
const ni = idx.nodeById.get(nodeId);
|
|
1128
|
+
if (ni === void 0) return null;
|
|
1129
|
+
const node = graph.nodes[ni];
|
|
1130
|
+
if (node.initialNodeId) return resolveInitial(node.initialNodeId, seen);
|
|
1131
|
+
const childIds = idx.childNodes.get(nodeId) ?? [];
|
|
1132
|
+
if (childIds.length > 0) return resolveInitial(childIds[0], seen);
|
|
1133
|
+
return nodeId;
|
|
1134
|
+
}
|
|
1135
|
+
function getLeafDescendants(nodeId) {
|
|
1136
|
+
if (leaves.has(nodeId)) return [nodeId];
|
|
1137
|
+
const result = [];
|
|
1138
|
+
const collect = (id) => {
|
|
1139
|
+
const childIds = idx.childNodes.get(id) ?? [];
|
|
1140
|
+
for (const childId of childIds) if (leaves.has(childId)) result.push(childId);
|
|
1141
|
+
else collect(childId);
|
|
1142
|
+
};
|
|
1143
|
+
collect(nodeId);
|
|
1144
|
+
return result;
|
|
1145
|
+
}
|
|
1146
|
+
const edgeSeen = /* @__PURE__ */ new Set();
|
|
1147
|
+
const flatEdges = [];
|
|
1148
|
+
for (const edge of graph.edges) {
|
|
1149
|
+
const sources = leaves.has(edge.sourceId) ? [edge.sourceId] : getLeafDescendants(edge.sourceId);
|
|
1150
|
+
const target = leaves.has(edge.targetId) ? edge.targetId : resolveInitial(edge.targetId);
|
|
1151
|
+
if (target === null) continue;
|
|
1152
|
+
for (const source of sources) {
|
|
1153
|
+
const isAuthoredLeafSelfLoop = edge.sourceId === edge.targetId && leaves.has(edge.sourceId);
|
|
1154
|
+
if (source === target && !isAuthoredLeafSelfLoop) continue;
|
|
1155
|
+
const key = `${source}->${target}`;
|
|
1156
|
+
if (edgeSeen.has(key)) continue;
|
|
1157
|
+
edgeSeen.add(key);
|
|
1158
|
+
flatEdges.push({
|
|
1159
|
+
type: "edge",
|
|
1160
|
+
id: `${edge.id}:${source}->${target}`,
|
|
1161
|
+
sourceId: source,
|
|
1162
|
+
targetId: target,
|
|
1163
|
+
label: edge.label,
|
|
1164
|
+
data: edge.data,
|
|
1165
|
+
...edge.weight !== void 0 && { weight: edge.weight },
|
|
1166
|
+
...edge.mode !== void 0 && { mode: edge.mode },
|
|
1167
|
+
...source === edge.sourceId && edge.sourcePort !== void 0 && { sourcePort: edge.sourcePort },
|
|
1168
|
+
...target === edge.targetId && edge.targetPort !== void 0 && { targetPort: edge.targetPort }
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
const leafNodes = graph.nodes.filter((n) => leaves.has(n.id)).map((n) => {
|
|
1173
|
+
const { type, parentId, initialNodeId, ...rest } = n;
|
|
1174
|
+
return rest;
|
|
1175
|
+
});
|
|
1176
|
+
return createGraph({
|
|
1177
|
+
id: graph.id,
|
|
1178
|
+
mode: graph.mode,
|
|
1179
|
+
initialNodeId: graph.initialNodeId ? resolveInitial(graph.initialNodeId) ?? void 0 : void 0,
|
|
1180
|
+
nodes: leafNodes,
|
|
1181
|
+
edges: flatEdges,
|
|
1182
|
+
data: graph.data
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
/**
|
|
1186
|
+
* @deprecated Use {@link getFlattenedGraph}.
|
|
1187
|
+
*/
|
|
1188
|
+
function flatten(graph) {
|
|
1189
|
+
return getFlattenedGraph(graph);
|
|
1190
|
+
}
|
|
1191
|
+
/**
|
|
1192
|
+
* Convert a node to a config, stripping parentId/initialNodeId references
|
|
1193
|
+
* to nodes outside the given set.
|
|
1194
|
+
*/
|
|
1195
|
+
function toScopedNodeConfig(node, nodeIdSet) {
|
|
1196
|
+
const config = toNodeConfig(node);
|
|
1197
|
+
if (nodeIdSet) {
|
|
1198
|
+
if (config.parentId != null && !nodeIdSet.has(config.parentId)) delete config.parentId;
|
|
1199
|
+
if (config.initialNodeId != null && !nodeIdSet.has(config.initialNodeId)) delete config.initialNodeId;
|
|
1200
|
+
}
|
|
1201
|
+
return config;
|
|
1202
|
+
}
|
|
1203
|
+
/**
|
|
1204
|
+
* Returns the induced subgraph containing only the given node IDs
|
|
1205
|
+
* and edges whose endpoints are both in the set.
|
|
1206
|
+
*
|
|
1207
|
+
* Parent references to nodes outside the set are removed.
|
|
1208
|
+
*
|
|
1209
|
+
* @example
|
|
1210
|
+
* ```ts
|
|
1211
|
+
* import { createGraph, getSubgraph } from '@statelyai/graph';
|
|
1212
|
+
*
|
|
1213
|
+
* const graph = createGraph({
|
|
1214
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1215
|
+
* edges: [
|
|
1216
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1217
|
+
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
1218
|
+
* ],
|
|
1219
|
+
* });
|
|
1220
|
+
*
|
|
1221
|
+
* const sub = getSubgraph(graph, ['a', 'b']);
|
|
1222
|
+
* // sub.nodes: [a, b], sub.edges: [ab]
|
|
1223
|
+
* ```
|
|
1224
|
+
*/
|
|
1225
|
+
function getSubgraph(graph, nodeIds) {
|
|
1226
|
+
const nodeIdSet = new Set(nodeIds);
|
|
1227
|
+
return createGraph({
|
|
1228
|
+
id: graph.id,
|
|
1229
|
+
mode: graph.mode,
|
|
1230
|
+
initialNodeId: graph.initialNodeId && nodeIdSet.has(graph.initialNodeId) ? graph.initialNodeId : void 0,
|
|
1231
|
+
nodes: graph.nodes.filter((n) => nodeIdSet.has(n.id)).map((n) => toScopedNodeConfig(n, nodeIdSet)),
|
|
1232
|
+
edges: graph.edges.filter((e) => nodeIdSet.has(e.sourceId) && nodeIdSet.has(e.targetId)).map(toEdgeConfig),
|
|
1233
|
+
data: graph.data
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
/**
|
|
1237
|
+
* Returns a new graph with all edge directions flipped (source ↔ target).
|
|
1238
|
+
* Optionally filters which edges to include.
|
|
1239
|
+
*
|
|
1240
|
+
* @example
|
|
1241
|
+
* ```ts
|
|
1242
|
+
* import { createGraph, getReversedGraph } from '@statelyai/graph';
|
|
1243
|
+
*
|
|
1244
|
+
* const graph = createGraph({
|
|
1245
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1246
|
+
* edges: [
|
|
1247
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1248
|
+
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
1249
|
+
* ],
|
|
1250
|
+
* });
|
|
1251
|
+
*
|
|
1252
|
+
* const rev = getReversedGraph(graph);
|
|
1253
|
+
* // rev edges: b→a, c→b
|
|
1254
|
+
*
|
|
1255
|
+
* const filtered = getReversedGraph(graph, (e) => e.id !== 'bc');
|
|
1256
|
+
* // filtered edges: b→a (only ab reversed, bc excluded)
|
|
1257
|
+
* ```
|
|
1258
|
+
*/
|
|
1259
|
+
function getReversedGraph(graph, filterEdge) {
|
|
1260
|
+
const edges = filterEdge ? graph.edges.filter(filterEdge) : graph.edges;
|
|
1261
|
+
return createGraph({
|
|
1262
|
+
id: graph.id,
|
|
1263
|
+
mode: graph.mode,
|
|
1264
|
+
initialNodeId: graph.initialNodeId ?? void 0,
|
|
1265
|
+
nodes: graph.nodes.map((n) => toNodeConfig(n)),
|
|
1266
|
+
edges: edges.map((e) => {
|
|
1267
|
+
const config = toEdgeConfig(e);
|
|
1268
|
+
config.sourceId = e.targetId;
|
|
1269
|
+
config.targetId = e.sourceId;
|
|
1270
|
+
delete config.sourcePort;
|
|
1271
|
+
delete config.targetPort;
|
|
1272
|
+
if (e.targetPort !== void 0) config.sourcePort = e.targetPort;
|
|
1273
|
+
if (e.sourcePort !== void 0) config.targetPort = e.sourcePort;
|
|
1274
|
+
return config;
|
|
1275
|
+
}),
|
|
1276
|
+
data: graph.data
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
/**
|
|
1280
|
+
* @deprecated Use {@link getReversedGraph}.
|
|
1281
|
+
*/
|
|
1282
|
+
function reverseGraph(graph, filterEdge) {
|
|
1283
|
+
return getReversedGraph(graph, filterEdge);
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
//#endregion
|
|
1287
|
+
//#region src/algorithms/traversal.ts
|
|
1288
|
+
function* genBFS(graph, startId) {
|
|
1289
|
+
const csr = getCSR(graph);
|
|
1290
|
+
const start = csr.indexOf.get(startId);
|
|
1291
|
+
if (start === void 0) return;
|
|
1292
|
+
const n = csr.ids.length;
|
|
1293
|
+
const visited = new Uint8Array(n);
|
|
1294
|
+
const queue = new Int32Array(n);
|
|
1295
|
+
visited[start] = 1;
|
|
1296
|
+
queue[0] = start;
|
|
1297
|
+
let head = 0;
|
|
1298
|
+
let tail = 1;
|
|
1299
|
+
while (head < tail) {
|
|
1300
|
+
const u = queue[head++];
|
|
1301
|
+
yield graph.nodes[u];
|
|
1302
|
+
for (let a = csr.outOffsets[u]; a < csr.outOffsets[u + 1]; a++) {
|
|
1303
|
+
const v = csr.outTargets[a];
|
|
1304
|
+
if (!visited[v]) {
|
|
1305
|
+
visited[v] = 1;
|
|
1306
|
+
queue[tail++] = v;
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* @deprecated Use {@link genBFS}.
|
|
1313
|
+
*/
|
|
1314
|
+
function* bfs(graph, startId) {
|
|
1315
|
+
yield* genBFS(graph, startId);
|
|
1316
|
+
}
|
|
1317
|
+
function* genDFS(graph, startId) {
|
|
1318
|
+
const csr = getCSR(graph);
|
|
1319
|
+
const start = csr.indexOf.get(startId);
|
|
1320
|
+
if (start === void 0) return;
|
|
1321
|
+
const n = csr.ids.length;
|
|
1322
|
+
const visited = new Uint8Array(n);
|
|
1323
|
+
const stack = [start];
|
|
1324
|
+
while (stack.length > 0) {
|
|
1325
|
+
const u = stack.pop();
|
|
1326
|
+
if (visited[u]) continue;
|
|
1327
|
+
visited[u] = 1;
|
|
1328
|
+
yield graph.nodes[u];
|
|
1329
|
+
for (let a = csr.outOffsets[u]; a < csr.outOffsets[u + 1]; a++) {
|
|
1330
|
+
const v = csr.outTargets[a];
|
|
1331
|
+
if (!visited[v]) stack.push(v);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
/**
|
|
1336
|
+
* @deprecated Use {@link genDFS}.
|
|
1337
|
+
*/
|
|
1338
|
+
function* dfs(graph, startId) {
|
|
1339
|
+
yield* genDFS(graph, startId);
|
|
1340
|
+
}
|
|
1341
|
+
function isAcyclic(graph) {
|
|
1342
|
+
const kind = getEffectiveModeKind(graph);
|
|
1343
|
+
if (kind === "mixed") return isAcyclicMixed(graph);
|
|
1344
|
+
if (kind === "non-directed") return isAcyclicUndirected(graph);
|
|
1345
|
+
const WHITE = 0;
|
|
1346
|
+
const GRAY = 1;
|
|
1347
|
+
const BLACK = 2;
|
|
1348
|
+
const color = /* @__PURE__ */ new Map();
|
|
1349
|
+
for (const node of graph.nodes) color.set(node.id, WHITE);
|
|
1350
|
+
const hasCycle = (id) => {
|
|
1351
|
+
color.set(id, GRAY);
|
|
1352
|
+
for (const neighborId of getSuccessorIds(graph, id)) {
|
|
1353
|
+
const current = color.get(neighborId);
|
|
1354
|
+
if (current === GRAY) return true;
|
|
1355
|
+
if (current === WHITE && hasCycle(neighborId)) return true;
|
|
1356
|
+
}
|
|
1357
|
+
color.set(id, BLACK);
|
|
1358
|
+
return false;
|
|
1359
|
+
};
|
|
1360
|
+
for (const node of graph.nodes) if (color.get(node.id) === WHITE && hasCycle(node.id)) return false;
|
|
1361
|
+
return true;
|
|
1362
|
+
}
|
|
1363
|
+
/**
|
|
1364
|
+
* Acyclicity for graphs mixing directed and non-directed edges.
|
|
1365
|
+
*
|
|
1366
|
+
* Polynomial fast paths first: a cycle among directed edges alone, a cycle
|
|
1367
|
+
* among non-directed edges alone (union-find), or all-singleton reachability
|
|
1368
|
+
* SCCs (then no mixed cycle can exist either). Only ambiguous multi-node
|
|
1369
|
+
* SCCs fall back to exact simple-cycle enumeration, restricted to that SCC.
|
|
1370
|
+
*/
|
|
1371
|
+
function isAcyclicMixed(graph) {
|
|
1372
|
+
const idx = getIndex(graph);
|
|
1373
|
+
const WHITE = 0;
|
|
1374
|
+
const GRAY = 1;
|
|
1375
|
+
const BLACK = 2;
|
|
1376
|
+
const color = /* @__PURE__ */ new Map();
|
|
1377
|
+
for (const node of graph.nodes) color.set(node.id, WHITE);
|
|
1378
|
+
const hasDirectedCycle = (id) => {
|
|
1379
|
+
color.set(id, GRAY);
|
|
1380
|
+
for (const eid of idx.outEdges.get(id) ?? []) {
|
|
1381
|
+
const edge = graph.edges[idx.edgeById.get(eid)];
|
|
1382
|
+
if (getEdgeMode(graph, edge) !== "directed") continue;
|
|
1383
|
+
const current = color.get(edge.targetId);
|
|
1384
|
+
if (current === GRAY) return true;
|
|
1385
|
+
if (current === WHITE && hasDirectedCycle(edge.targetId)) return true;
|
|
1386
|
+
}
|
|
1387
|
+
color.set(id, BLACK);
|
|
1388
|
+
return false;
|
|
1389
|
+
};
|
|
1390
|
+
for (const node of graph.nodes) if (color.get(node.id) === WHITE && hasDirectedCycle(node.id)) return false;
|
|
1391
|
+
const parent = /* @__PURE__ */ new Map();
|
|
1392
|
+
const find = (id) => {
|
|
1393
|
+
let root = id;
|
|
1394
|
+
while (parent.get(root) !== root) root = parent.get(root);
|
|
1395
|
+
let cursor = id;
|
|
1396
|
+
while (parent.get(cursor) !== root) {
|
|
1397
|
+
const next = parent.get(cursor);
|
|
1398
|
+
parent.set(cursor, root);
|
|
1399
|
+
cursor = next;
|
|
1400
|
+
}
|
|
1401
|
+
return root;
|
|
1402
|
+
};
|
|
1403
|
+
for (const node of graph.nodes) parent.set(node.id, node.id);
|
|
1404
|
+
for (const edge of graph.edges) {
|
|
1405
|
+
if (getEdgeMode(graph, edge) === "directed") continue;
|
|
1406
|
+
if (edge.sourceId === edge.targetId) return false;
|
|
1407
|
+
const rootA = find(edge.sourceId);
|
|
1408
|
+
const rootB = find(edge.targetId);
|
|
1409
|
+
if (rootA === rootB) return false;
|
|
1410
|
+
parent.set(rootA, rootB);
|
|
1411
|
+
}
|
|
1412
|
+
const multiNodeSccs = getStronglyConnectedComponents(graph).filter((component) => component.length > 1);
|
|
1413
|
+
if (multiNodeSccs.length === 0) return true;
|
|
1414
|
+
for (const component of multiNodeSccs) {
|
|
1415
|
+
const subgraph = getSubgraph(graph, component.map((node) => node.id));
|
|
1416
|
+
for (const _cycle of genCycles(subgraph)) return false;
|
|
1417
|
+
}
|
|
1418
|
+
return true;
|
|
1419
|
+
}
|
|
1420
|
+
function isAcyclicUndirected(graph) {
|
|
1421
|
+
const idx = getIndex(graph);
|
|
1422
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1423
|
+
const hasCycle = (id, parentId) => {
|
|
1424
|
+
visited.add(id);
|
|
1425
|
+
for (const eid of idx.outEdges.get(id) ?? []) {
|
|
1426
|
+
const ai = idx.edgeById.get(eid);
|
|
1427
|
+
if (ai === void 0) continue;
|
|
1428
|
+
const neighborId = graph.edges[ai].targetId;
|
|
1429
|
+
if (!visited.has(neighborId)) {
|
|
1430
|
+
if (hasCycle(neighborId, id)) return true;
|
|
1431
|
+
} else if (neighborId !== parentId) return true;
|
|
1432
|
+
}
|
|
1433
|
+
for (const eid of idx.inEdges.get(id) ?? []) {
|
|
1434
|
+
const ai = idx.edgeById.get(eid);
|
|
1435
|
+
if (ai === void 0) continue;
|
|
1436
|
+
const neighborId = graph.edges[ai].sourceId;
|
|
1437
|
+
if (!visited.has(neighborId)) {
|
|
1438
|
+
if (hasCycle(neighborId, id)) return true;
|
|
1439
|
+
} else if (neighborId !== parentId) return true;
|
|
1440
|
+
}
|
|
1441
|
+
return false;
|
|
1442
|
+
};
|
|
1443
|
+
for (const node of graph.nodes) if (!visited.has(node.id) && hasCycle(node.id, null)) return false;
|
|
1444
|
+
return true;
|
|
1445
|
+
}
|
|
1446
|
+
function getConnectedComponents(graph) {
|
|
1447
|
+
const csr = getCSR(graph);
|
|
1448
|
+
const n = csr.ids.length;
|
|
1449
|
+
const visited = new Uint8Array(n);
|
|
1450
|
+
const queue = new Int32Array(n);
|
|
1451
|
+
const components = [];
|
|
1452
|
+
for (let s = 0; s < n; s++) {
|
|
1453
|
+
if (visited[s]) continue;
|
|
1454
|
+
const component = [];
|
|
1455
|
+
visited[s] = 1;
|
|
1456
|
+
queue[0] = s;
|
|
1457
|
+
let head = 0;
|
|
1458
|
+
let tail = 1;
|
|
1459
|
+
while (head < tail) {
|
|
1460
|
+
const u = queue[head++];
|
|
1461
|
+
component.push(graph.nodes[u]);
|
|
1462
|
+
for (let a = csr.outOffsets[u]; a < csr.outOffsets[u + 1]; a++) {
|
|
1463
|
+
const v = csr.outTargets[a];
|
|
1464
|
+
if (!visited[v]) {
|
|
1465
|
+
visited[v] = 1;
|
|
1466
|
+
queue[tail++] = v;
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
for (let a = csr.inOffsets[u]; a < csr.inOffsets[u + 1]; a++) {
|
|
1470
|
+
const v = csr.inOrigins[a];
|
|
1471
|
+
if (!visited[v]) {
|
|
1472
|
+
visited[v] = 1;
|
|
1473
|
+
queue[tail++] = v;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
components.push(component);
|
|
1478
|
+
}
|
|
1479
|
+
return components;
|
|
1480
|
+
}
|
|
1481
|
+
/**
|
|
1482
|
+
* Returns a topological ordering of the graph's nodes, or `null` if no such
|
|
1483
|
+
* ordering exists.
|
|
1484
|
+
*
|
|
1485
|
+
* Any edge whose effective mode (per {@link getEdgeMode}) is not `'directed'`
|
|
1486
|
+
* makes ordering impossible — an undirected/bidirectional edge is mutual
|
|
1487
|
+
* precedence, i.e. a 2-cycle — so the function returns `null`.
|
|
1488
|
+
*/
|
|
1489
|
+
function getTopologicalSort(graph) {
|
|
1490
|
+
for (const edge of graph.edges) if (getEdgeMode(graph, edge) !== "directed") return null;
|
|
1491
|
+
const idx = getIndex(graph);
|
|
1492
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
1493
|
+
for (const node of graph.nodes) inDegree.set(node.id, 0);
|
|
1494
|
+
for (const edge of graph.edges) inDegree.set(edge.targetId, (inDegree.get(edge.targetId) ?? 0) + 1);
|
|
1495
|
+
const queue = [];
|
|
1496
|
+
for (const [id, degree] of inDegree) if (degree === 0) queue.push(id);
|
|
1497
|
+
const result = [];
|
|
1498
|
+
while (queue.length > 0) {
|
|
1499
|
+
const id = queue.shift();
|
|
1500
|
+
const ni = idx.nodeById.get(id);
|
|
1501
|
+
if (ni !== void 0) result.push(graph.nodes[ni]);
|
|
1502
|
+
for (const eid of idx.outEdges.get(id) ?? []) {
|
|
1503
|
+
const ai = idx.edgeById.get(eid);
|
|
1504
|
+
if (ai === void 0) continue;
|
|
1505
|
+
const targetId = graph.edges[ai].targetId;
|
|
1506
|
+
const nextDegree = (inDegree.get(targetId) ?? 1) - 1;
|
|
1507
|
+
inDegree.set(targetId, nextDegree);
|
|
1508
|
+
if (nextDegree === 0) queue.push(targetId);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
if (result.length !== graph.nodes.length) return null;
|
|
1512
|
+
return result;
|
|
1513
|
+
}
|
|
1514
|
+
function hasPath(graph, sourceId, targetId) {
|
|
1515
|
+
if (sourceId === targetId) return true;
|
|
1516
|
+
const visited = new Set([sourceId]);
|
|
1517
|
+
const queue = [sourceId];
|
|
1518
|
+
while (queue.length > 0) {
|
|
1519
|
+
const id = queue.shift();
|
|
1520
|
+
for (const neighborId of getNeighborIds(graph, id)) {
|
|
1521
|
+
if (neighborId === targetId) return true;
|
|
1522
|
+
if (!visited.has(neighborId)) {
|
|
1523
|
+
visited.add(neighborId);
|
|
1524
|
+
queue.push(neighborId);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
return false;
|
|
1529
|
+
}
|
|
1530
|
+
function isConnected(graph) {
|
|
1531
|
+
if (graph.nodes.length === 0) return true;
|
|
1532
|
+
return getConnectedComponents(graph).length <= 1;
|
|
1533
|
+
}
|
|
1534
|
+
/**
|
|
1535
|
+
* Returns whether the graph is a tree: connected, acyclic, and with exactly
|
|
1536
|
+
* `nodes.length - 1` edges (so directed diamonds and parallel edges are not
|
|
1537
|
+
* trees). Empty and single-node graphs are considered trees.
|
|
1538
|
+
*/
|
|
1539
|
+
function isTree(graph) {
|
|
1540
|
+
if (graph.nodes.length === 0) return true;
|
|
1541
|
+
return graph.edges.length === graph.nodes.length - 1 && isConnected(graph) && isAcyclic(graph);
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
//#endregion
|
|
1545
|
+
//#region src/algorithms/ordering.ts
|
|
1546
|
+
function getPreorder(graph, opts) {
|
|
1547
|
+
const idx = getIndex(graph);
|
|
1548
|
+
const startId = resolveFrom(graph, opts);
|
|
1549
|
+
const startNi = idx.nodeById.get(startId);
|
|
1550
|
+
if (startNi === void 0) return [];
|
|
1551
|
+
const visited = new Set([startId]);
|
|
1552
|
+
const result = [graph.nodes[startNi]];
|
|
1553
|
+
const stack = [startId];
|
|
1554
|
+
while (stack.length > 0) {
|
|
1555
|
+
const top = stack[stack.length - 1];
|
|
1556
|
+
const next = getNeighborIds(graph, top).find((id) => !visited.has(id));
|
|
1557
|
+
if (next === void 0) {
|
|
1558
|
+
stack.pop();
|
|
1559
|
+
continue;
|
|
1560
|
+
}
|
|
1561
|
+
visited.add(next);
|
|
1562
|
+
stack.push(next);
|
|
1563
|
+
const ni = idx.nodeById.get(next);
|
|
1564
|
+
if (ni !== void 0) result.push(graph.nodes[ni]);
|
|
1565
|
+
}
|
|
1566
|
+
return result;
|
|
1567
|
+
}
|
|
1568
|
+
function getPostorder(graph, opts) {
|
|
1569
|
+
const idx = getIndex(graph);
|
|
1570
|
+
const startId = resolveFrom(graph, opts);
|
|
1571
|
+
if (idx.nodeById.get(startId) === void 0) return [];
|
|
1572
|
+
const visited = new Set([startId]);
|
|
1573
|
+
const result = [];
|
|
1574
|
+
const stack = [startId];
|
|
1575
|
+
while (stack.length > 0) {
|
|
1576
|
+
const top = stack[stack.length - 1];
|
|
1577
|
+
const next = getNeighborIds(graph, top).find((id) => !visited.has(id));
|
|
1578
|
+
if (next === void 0) {
|
|
1579
|
+
stack.pop();
|
|
1580
|
+
const ni = idx.nodeById.get(top);
|
|
1581
|
+
if (ni !== void 0) result.push(graph.nodes[ni]);
|
|
1582
|
+
continue;
|
|
1583
|
+
}
|
|
1584
|
+
visited.add(next);
|
|
1585
|
+
stack.push(next);
|
|
1586
|
+
}
|
|
1587
|
+
return result;
|
|
1588
|
+
}
|
|
1589
|
+
function getPreorders(graph, opts) {
|
|
1590
|
+
return [...genPreorders(graph, opts)];
|
|
1591
|
+
}
|
|
1592
|
+
function getPostorders(graph, opts) {
|
|
1593
|
+
return [...genPostorders(graph, opts)];
|
|
1594
|
+
}
|
|
1595
|
+
function* genPreorders(graph, opts) {
|
|
1596
|
+
const idx = getIndex(graph);
|
|
1597
|
+
const startId = resolveFrom(graph, opts);
|
|
1598
|
+
const startNi = idx.nodeById.get(startId);
|
|
1599
|
+
const startNode = startNi !== void 0 ? graph.nodes[startNi] : void 0;
|
|
1600
|
+
if (!startNode) return;
|
|
1601
|
+
const queue = [{
|
|
1602
|
+
visited: new Set([startId]),
|
|
1603
|
+
preorder: [startNode],
|
|
1604
|
+
dfsStack: [startId]
|
|
1605
|
+
}];
|
|
1606
|
+
while (queue.length > 0) {
|
|
1607
|
+
const frame = queue.pop();
|
|
1608
|
+
const { visited, dfsStack } = frame;
|
|
1609
|
+
let { preorder } = frame;
|
|
1610
|
+
let branched = false;
|
|
1611
|
+
while (dfsStack.length > 0) {
|
|
1612
|
+
const top = dfsStack[dfsStack.length - 1];
|
|
1613
|
+
const unvisited = getNeighborIds(graph, top).filter((id) => !visited.has(id));
|
|
1614
|
+
if (unvisited.length === 0) {
|
|
1615
|
+
dfsStack.pop();
|
|
1616
|
+
continue;
|
|
1617
|
+
}
|
|
1618
|
+
for (const nextId of unvisited) {
|
|
1619
|
+
const ni = idx.nodeById.get(nextId);
|
|
1620
|
+
if (ni === void 0) continue;
|
|
1621
|
+
const newVisited = new Set(visited);
|
|
1622
|
+
newVisited.add(nextId);
|
|
1623
|
+
queue.push({
|
|
1624
|
+
visited: newVisited,
|
|
1625
|
+
preorder: [...preorder, graph.nodes[ni]],
|
|
1626
|
+
dfsStack: [...dfsStack, nextId]
|
|
1627
|
+
});
|
|
1628
|
+
}
|
|
1629
|
+
branched = true;
|
|
1630
|
+
break;
|
|
1631
|
+
}
|
|
1632
|
+
if (!branched) yield preorder;
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
function* genPostorders(graph, opts) {
|
|
1636
|
+
const idx = getIndex(graph);
|
|
1637
|
+
const startId = resolveFrom(graph, opts);
|
|
1638
|
+
if (idx.nodeById.get(startId) === void 0) return;
|
|
1639
|
+
const queue = [{
|
|
1640
|
+
visited: new Set([startId]),
|
|
1641
|
+
postorder: [],
|
|
1642
|
+
dfsStack: [startId]
|
|
1643
|
+
}];
|
|
1644
|
+
while (queue.length > 0) {
|
|
1645
|
+
const frame = queue.pop();
|
|
1646
|
+
const { visited, dfsStack } = frame;
|
|
1647
|
+
let { postorder } = frame;
|
|
1648
|
+
let branched = false;
|
|
1649
|
+
while (dfsStack.length > 0) {
|
|
1650
|
+
const top = dfsStack[dfsStack.length - 1];
|
|
1651
|
+
const unvisited = getNeighborIds(graph, top).filter((id) => !visited.has(id));
|
|
1652
|
+
if (unvisited.length === 0) {
|
|
1653
|
+
dfsStack.pop();
|
|
1654
|
+
const ni = idx.nodeById.get(top);
|
|
1655
|
+
if (ni !== void 0) postorder = [...postorder, graph.nodes[ni]];
|
|
1656
|
+
continue;
|
|
1657
|
+
}
|
|
1658
|
+
for (const nextId of unvisited) {
|
|
1659
|
+
if (idx.nodeById.get(nextId) === void 0) continue;
|
|
1660
|
+
const newVisited = new Set(visited);
|
|
1661
|
+
newVisited.add(nextId);
|
|
1662
|
+
queue.push({
|
|
1663
|
+
visited: newVisited,
|
|
1664
|
+
postorder: [...postorder],
|
|
1665
|
+
dfsStack: [...dfsStack, nextId]
|
|
1666
|
+
});
|
|
1667
|
+
}
|
|
1668
|
+
branched = true;
|
|
1669
|
+
break;
|
|
1670
|
+
}
|
|
1671
|
+
if (!branched) yield postorder;
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
//#endregion
|
|
1676
|
+
//#region src/algorithms/spanning-tree.ts
|
|
1677
|
+
function getMinimumSpanningTree(graph, opts) {
|
|
1678
|
+
const algorithm = opts?.algorithm ?? "prim";
|
|
1679
|
+
const getWeight = opts?.getWeight ?? ((edge) => edge.weight ?? 1);
|
|
1680
|
+
const mstEdges = algorithm === "kruskal" ? kruskalMST(graph, getWeight) : primMST(graph, getWeight);
|
|
1681
|
+
return createGraph({
|
|
1682
|
+
id: graph.id,
|
|
1683
|
+
mode: graph.mode,
|
|
1684
|
+
initialNodeId: graph.initialNodeId ?? void 0,
|
|
1685
|
+
nodes: graph.nodes.map((node) => toNodeConfig(node)),
|
|
1686
|
+
edges: mstEdges.map((edge) => toEdgeConfig(edge))
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
function primMST(graph, getWeight) {
|
|
1690
|
+
if (graph.nodes.length === 0) return [];
|
|
1691
|
+
const idx = getIndex(graph);
|
|
1692
|
+
const inMST = /* @__PURE__ */ new Set();
|
|
1693
|
+
const mstEdges = [];
|
|
1694
|
+
const candidates = new MinPriorityQueue((a, b) => a.weight - b.weight);
|
|
1695
|
+
function addEdgesOf(nodeId) {
|
|
1696
|
+
for (const eid of idx.outEdges.get(nodeId) ?? []) {
|
|
1697
|
+
const ai = idx.edgeById.get(eid);
|
|
1698
|
+
if (ai === void 0) continue;
|
|
1699
|
+
const edge = graph.edges[ai];
|
|
1700
|
+
if (!inMST.has(edge.targetId)) candidates.push({
|
|
1701
|
+
weight: getWeight(edge),
|
|
1702
|
+
edge
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
for (const eid of idx.inEdges.get(nodeId) ?? []) {
|
|
1706
|
+
const ai = idx.edgeById.get(eid);
|
|
1707
|
+
if (ai === void 0) continue;
|
|
1708
|
+
const edge = graph.edges[ai];
|
|
1709
|
+
if (getEdgeMode(graph, edge) !== "directed" && !inMST.has(edge.sourceId)) candidates.push({
|
|
1710
|
+
weight: getWeight(edge),
|
|
1711
|
+
edge
|
|
1712
|
+
});
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
for (const node of graph.nodes) {
|
|
1716
|
+
if (inMST.has(node.id)) continue;
|
|
1717
|
+
inMST.add(node.id);
|
|
1718
|
+
addEdgesOf(node.id);
|
|
1719
|
+
while (candidates.size > 0 && inMST.size < graph.nodes.length) {
|
|
1720
|
+
const { edge } = candidates.pop();
|
|
1721
|
+
const targetId = getEdgeMode(graph, edge) !== "directed" && inMST.has(edge.targetId) ? edge.sourceId : edge.targetId;
|
|
1722
|
+
if (inMST.has(targetId)) continue;
|
|
1723
|
+
inMST.add(targetId);
|
|
1724
|
+
mstEdges.push(edge);
|
|
1725
|
+
addEdgesOf(targetId);
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
return mstEdges;
|
|
1729
|
+
}
|
|
1730
|
+
function kruskalMST(graph, getWeight) {
|
|
1731
|
+
const sorted = [...graph.edges].sort((a, b) => getWeight(a) - getWeight(b));
|
|
1732
|
+
const parent = /* @__PURE__ */ new Map();
|
|
1733
|
+
const rank = /* @__PURE__ */ new Map();
|
|
1734
|
+
for (const node of graph.nodes) {
|
|
1735
|
+
parent.set(node.id, node.id);
|
|
1736
|
+
rank.set(node.id, 0);
|
|
1737
|
+
}
|
|
1738
|
+
function find(id) {
|
|
1739
|
+
if (parent.get(id) !== id) parent.set(id, find(parent.get(id)));
|
|
1740
|
+
return parent.get(id);
|
|
1741
|
+
}
|
|
1742
|
+
function union(a, b) {
|
|
1743
|
+
const rootA = find(a);
|
|
1744
|
+
const rootB = find(b);
|
|
1745
|
+
if (rootA === rootB) return false;
|
|
1746
|
+
if (rank.get(rootA) < rank.get(rootB)) parent.set(rootA, rootB);
|
|
1747
|
+
else if (rank.get(rootA) > rank.get(rootB)) parent.set(rootB, rootA);
|
|
1748
|
+
else {
|
|
1749
|
+
parent.set(rootB, rootA);
|
|
1750
|
+
rank.set(rootA, rank.get(rootA) + 1);
|
|
1751
|
+
}
|
|
1752
|
+
return true;
|
|
1753
|
+
}
|
|
1754
|
+
const mstEdges = [];
|
|
1755
|
+
for (const edge of sorted) if (union(edge.sourceId, edge.targetId)) mstEdges.push(edge);
|
|
1756
|
+
return mstEdges;
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
//#endregion
|
|
1760
|
+
//#region src/algorithms/centrality.ts
|
|
1761
|
+
function getNodeIds(graph) {
|
|
1762
|
+
return graph.nodes.map((node) => node.id);
|
|
1763
|
+
}
|
|
1764
|
+
function createEmptyScoreMap(graph) {
|
|
1765
|
+
return Object.fromEntries(graph.nodes.map((node) => [node.id, 0]));
|
|
1766
|
+
}
|
|
1767
|
+
function normalizeTypedVector(values) {
|
|
1768
|
+
let sumOfSquares = 0;
|
|
1769
|
+
for (let i = 0; i < values.length; i++) sumOfSquares += values[i] * values[i];
|
|
1770
|
+
const magnitude = Math.sqrt(sumOfSquares);
|
|
1771
|
+
if (magnitude === 0) return;
|
|
1772
|
+
for (let i = 0; i < values.length; i++) values[i] /= magnitude;
|
|
1773
|
+
}
|
|
1774
|
+
/**
|
|
1775
|
+
* BFS hop distances from a start position over the CSR arc snapshot.
|
|
1776
|
+
* `dist[i] === -1` means unreachable. Returns the visit count.
|
|
1777
|
+
*/
|
|
1778
|
+
function bfsDistances(csr, start, dist, queue) {
|
|
1779
|
+
dist.fill(-1);
|
|
1780
|
+
dist[start] = 0;
|
|
1781
|
+
queue[0] = start;
|
|
1782
|
+
let head = 0;
|
|
1783
|
+
let tail = 1;
|
|
1784
|
+
while (head < tail) {
|
|
1785
|
+
const u = queue[head++];
|
|
1786
|
+
const du = dist[u];
|
|
1787
|
+
for (let a = csr.outOffsets[u]; a < csr.outOffsets[u + 1]; a++) {
|
|
1788
|
+
const v = csr.outTargets[a];
|
|
1789
|
+
if (dist[v] === -1) {
|
|
1790
|
+
dist[v] = du + 1;
|
|
1791
|
+
queue[tail++] = v;
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
return tail;
|
|
1796
|
+
}
|
|
1797
|
+
/**
|
|
1798
|
+
* Returns degree centrality scores for all nodes.
|
|
1799
|
+
*
|
|
1800
|
+
* Degree centrality is the node degree normalized by `n - 1`.
|
|
1801
|
+
*
|
|
1802
|
+
* @example
|
|
1803
|
+
* ```ts
|
|
1804
|
+
* const scores = getDegreeCentrality(graph);
|
|
1805
|
+
* console.log(scores.a); // 0.5
|
|
1806
|
+
* ```
|
|
1807
|
+
*/
|
|
1808
|
+
function getDegreeCentrality(graph) {
|
|
1809
|
+
const scale = graph.nodes.length > 1 ? 1 / (graph.nodes.length - 1) : 0;
|
|
1810
|
+
const scores = createEmptyScoreMap(graph);
|
|
1811
|
+
for (const node of graph.nodes) scores[node.id] = getDegree(graph, node.id) * scale;
|
|
1812
|
+
return scores;
|
|
1813
|
+
}
|
|
1814
|
+
/**
|
|
1815
|
+
* Returns in-degree centrality scores for all nodes.
|
|
1816
|
+
*
|
|
1817
|
+
* In-degree centrality is the incoming degree normalized by `n - 1`.
|
|
1818
|
+
*/
|
|
1819
|
+
function getInDegreeCentrality(graph) {
|
|
1820
|
+
const scale = graph.nodes.length > 1 ? 1 / (graph.nodes.length - 1) : 0;
|
|
1821
|
+
const scores = createEmptyScoreMap(graph);
|
|
1822
|
+
for (const node of graph.nodes) scores[node.id] = getInDegree(graph, node.id) * scale;
|
|
1823
|
+
return scores;
|
|
1824
|
+
}
|
|
1825
|
+
/**
|
|
1826
|
+
* Returns out-degree centrality scores for all nodes.
|
|
1827
|
+
*
|
|
1828
|
+
* Out-degree centrality is the outgoing degree normalized by `n - 1`.
|
|
1829
|
+
*/
|
|
1830
|
+
function getOutDegreeCentrality(graph) {
|
|
1831
|
+
const scale = graph.nodes.length > 1 ? 1 / (graph.nodes.length - 1) : 0;
|
|
1832
|
+
const scores = createEmptyScoreMap(graph);
|
|
1833
|
+
for (const node of graph.nodes) scores[node.id] = getOutDegree(graph, node.id) * scale;
|
|
1834
|
+
return scores;
|
|
1835
|
+
}
|
|
1836
|
+
/**
|
|
1837
|
+
* Returns closeness centrality scores for all nodes.
|
|
1838
|
+
*
|
|
1839
|
+
* Distances are computed over unweighted shortest paths using the graph's
|
|
1840
|
+
* existing directed or undirected edge semantics.
|
|
1841
|
+
*/
|
|
1842
|
+
function getClosenessCentrality(graph) {
|
|
1843
|
+
const scores = createEmptyScoreMap(graph);
|
|
1844
|
+
const csr = getCSR(graph);
|
|
1845
|
+
const order = csr.ids.length;
|
|
1846
|
+
const dist = new Int32Array(order);
|
|
1847
|
+
const queue = new Int32Array(order);
|
|
1848
|
+
for (let s = 0; s < order; s++) {
|
|
1849
|
+
const visited = bfsDistances(csr, s, dist, queue);
|
|
1850
|
+
const reachable = visited - 1;
|
|
1851
|
+
if (reachable === 0) continue;
|
|
1852
|
+
let totalDistance = 0;
|
|
1853
|
+
for (let k = 0; k < visited; k++) totalDistance += dist[queue[k]];
|
|
1854
|
+
if (totalDistance === 0) continue;
|
|
1855
|
+
const closeness = reachable / totalDistance;
|
|
1856
|
+
scores[csr.ids[s]] = order > 1 ? closeness * (reachable / (order - 1)) : closeness;
|
|
1857
|
+
}
|
|
1858
|
+
return scores;
|
|
1859
|
+
}
|
|
1860
|
+
/**
|
|
1861
|
+
* Returns betweenness centrality scores for all nodes.
|
|
1862
|
+
*
|
|
1863
|
+
* Uses Brandes' algorithm over unweighted shortest paths and returns
|
|
1864
|
+
* normalized scores.
|
|
1865
|
+
*/
|
|
1866
|
+
function getBetweennessCentrality(graph) {
|
|
1867
|
+
const csr = getCSR(graph);
|
|
1868
|
+
const n = csr.ids.length;
|
|
1869
|
+
const totals = new Float64Array(n);
|
|
1870
|
+
const sigma = new Float64Array(n);
|
|
1871
|
+
const dist = new Int32Array(n);
|
|
1872
|
+
const delta = new Float64Array(n);
|
|
1873
|
+
const order_ = new Int32Array(n);
|
|
1874
|
+
for (let s = 0; s < n; s++) {
|
|
1875
|
+
sigma.fill(0);
|
|
1876
|
+
dist.fill(-1);
|
|
1877
|
+
delta.fill(0);
|
|
1878
|
+
sigma[s] = 1;
|
|
1879
|
+
dist[s] = 0;
|
|
1880
|
+
order_[0] = s;
|
|
1881
|
+
let head = 0;
|
|
1882
|
+
let tail = 1;
|
|
1883
|
+
while (head < tail) {
|
|
1884
|
+
const u = order_[head++];
|
|
1885
|
+
const du = dist[u];
|
|
1886
|
+
for (let a = csr.outOffsets[u]; a < csr.outOffsets[u + 1]; a++) {
|
|
1887
|
+
const v = csr.outTargets[a];
|
|
1888
|
+
if (dist[v] === -1) {
|
|
1889
|
+
dist[v] = du + 1;
|
|
1890
|
+
order_[tail++] = v;
|
|
1891
|
+
}
|
|
1892
|
+
if (dist[v] === du + 1) sigma[v] += sigma[u];
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
for (let k = tail - 1; k >= 0; k--) {
|
|
1896
|
+
const w = order_[k];
|
|
1897
|
+
const sigmaW = sigma[w];
|
|
1898
|
+
if (sigmaW === 0) continue;
|
|
1899
|
+
const coefficient = (1 + delta[w]) / sigmaW;
|
|
1900
|
+
for (let a = csr.inOffsets[w]; a < csr.inOffsets[w + 1]; a++) {
|
|
1901
|
+
const v = csr.inOrigins[a];
|
|
1902
|
+
if (dist[v] === dist[w] - 1) delta[v] += sigma[v] * coefficient;
|
|
1903
|
+
}
|
|
1904
|
+
if (w !== s) totals[w] += delta[w];
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
const scores = createEmptyScoreMap(graph);
|
|
1908
|
+
for (let i = 0; i < n; i++) scores[csr.ids[i]] = totals[i];
|
|
1909
|
+
const order = graph.nodes.length;
|
|
1910
|
+
if (order <= 2) return scores;
|
|
1911
|
+
const scale = graph.mode !== "directed" ? 1 / ((order - 1) * (order - 2) / 2) : 1 / ((order - 1) * (order - 2));
|
|
1912
|
+
for (const nodeId of Object.keys(scores)) {
|
|
1913
|
+
if (graph.mode !== "directed") scores[nodeId] /= 2;
|
|
1914
|
+
scores[nodeId] *= scale;
|
|
1915
|
+
}
|
|
1916
|
+
return scores;
|
|
1917
|
+
}
|
|
1918
|
+
/**
|
|
1919
|
+
* Returns PageRank scores for all nodes.
|
|
1920
|
+
*
|
|
1921
|
+
* Uses power iteration with damping factor `alpha`.
|
|
1922
|
+
*/
|
|
1923
|
+
function getPageRank(graph, options) {
|
|
1924
|
+
const nodeIds = getNodeIds(graph);
|
|
1925
|
+
if (nodeIds.length === 0) return {};
|
|
1926
|
+
const alpha = options?.alpha ?? .85;
|
|
1927
|
+
const maxIterations = options?.maxIterations ?? 100;
|
|
1928
|
+
const tolerance = options?.tolerance ?? 1e-6;
|
|
1929
|
+
let scores = Object.fromEntries(nodeIds.map((nodeId) => [nodeId, 1 / nodeIds.length]));
|
|
1930
|
+
const csr = getCSR(graph);
|
|
1931
|
+
const n = csr.ids.length;
|
|
1932
|
+
let current = new Float64Array(n).fill(1 / n);
|
|
1933
|
+
for (let i = 0; i < n; i++) current[i] = scores[csr.ids[i]];
|
|
1934
|
+
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
1935
|
+
const next = new Float64Array(n).fill((1 - alpha) / n);
|
|
1936
|
+
let danglingMass = 0;
|
|
1937
|
+
for (let u = 0; u < n; u++) {
|
|
1938
|
+
const arcCount = csr.outOffsets[u + 1] - csr.outOffsets[u];
|
|
1939
|
+
if (arcCount === 0) {
|
|
1940
|
+
danglingMass += current[u];
|
|
1941
|
+
continue;
|
|
1942
|
+
}
|
|
1943
|
+
const share = alpha * current[u] / arcCount;
|
|
1944
|
+
for (let a = csr.outOffsets[u]; a < csr.outOffsets[u + 1]; a++) next[csr.outTargets[a]] += share;
|
|
1945
|
+
}
|
|
1946
|
+
if (danglingMass > 0) {
|
|
1947
|
+
const share = alpha * danglingMass / n;
|
|
1948
|
+
for (let i = 0; i < n; i++) next[i] += share;
|
|
1949
|
+
}
|
|
1950
|
+
let diff = 0;
|
|
1951
|
+
for (let i = 0; i < n; i++) diff = Math.max(diff, Math.abs(current[i] - next[i]));
|
|
1952
|
+
current = next;
|
|
1953
|
+
if (diff <= tolerance) break;
|
|
1954
|
+
}
|
|
1955
|
+
for (let i = 0; i < n; i++) scores[csr.ids[i]] = current[i];
|
|
1956
|
+
const total = Object.values(scores).reduce((sum, value) => sum + value, 0);
|
|
1957
|
+
if (total !== 0) for (const nodeId of nodeIds) scores[nodeId] /= total;
|
|
1958
|
+
return scores;
|
|
1959
|
+
}
|
|
1960
|
+
/**
|
|
1961
|
+
* Returns HITS hub and authority scores for all nodes.
|
|
1962
|
+
*
|
|
1963
|
+
* Uses power iteration and L2 normalization per iteration.
|
|
1964
|
+
*/
|
|
1965
|
+
function getHITS(graph, options) {
|
|
1966
|
+
if (getNodeIds(graph).length === 0) return {
|
|
1967
|
+
hubs: {},
|
|
1968
|
+
authorities: {}
|
|
1969
|
+
};
|
|
1970
|
+
const maxIterations = options?.maxIterations ?? 100;
|
|
1971
|
+
const tolerance = options?.tolerance ?? 1e-6;
|
|
1972
|
+
const csr = getCSR(graph);
|
|
1973
|
+
const n = csr.ids.length;
|
|
1974
|
+
let hubs = new Float64Array(n).fill(1);
|
|
1975
|
+
let authorities = new Float64Array(n);
|
|
1976
|
+
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
1977
|
+
const nextAuthorities = new Float64Array(n);
|
|
1978
|
+
for (let w = 0; w < n; w++) for (let a = csr.inOffsets[w]; a < csr.inOffsets[w + 1]; a++) nextAuthorities[w] += hubs[csr.inOrigins[a]];
|
|
1979
|
+
normalizeTypedVector(nextAuthorities);
|
|
1980
|
+
const nextHubs = new Float64Array(n);
|
|
1981
|
+
for (let u = 0; u < n; u++) for (let a = csr.outOffsets[u]; a < csr.outOffsets[u + 1]; a++) nextHubs[u] += nextAuthorities[csr.outTargets[a]];
|
|
1982
|
+
normalizeTypedVector(nextHubs);
|
|
1983
|
+
let diff = 0;
|
|
1984
|
+
for (let i = 0; i < n; i++) diff = Math.max(diff, Math.abs(hubs[i] - nextHubs[i]), Math.abs(authorities[i] - nextAuthorities[i]));
|
|
1985
|
+
hubs = nextHubs;
|
|
1986
|
+
authorities = nextAuthorities;
|
|
1987
|
+
if (diff <= tolerance) break;
|
|
1988
|
+
}
|
|
1989
|
+
const hubScores = createEmptyScoreMap(graph);
|
|
1990
|
+
const authorityScores = createEmptyScoreMap(graph);
|
|
1991
|
+
for (let i = 0; i < n; i++) {
|
|
1992
|
+
hubScores[csr.ids[i]] = hubs[i];
|
|
1993
|
+
authorityScores[csr.ids[i]] = authorities[i];
|
|
1994
|
+
}
|
|
1995
|
+
return {
|
|
1996
|
+
hubs: hubScores,
|
|
1997
|
+
authorities: authorityScores
|
|
1998
|
+
};
|
|
1999
|
+
}
|
|
2000
|
+
/**
|
|
2001
|
+
* Returns eigenvector centrality scores for all nodes.
|
|
2002
|
+
*
|
|
2003
|
+
* Power iteration with the `A + I` shift (same scheme as graphology and
|
|
2004
|
+
* networkx, so bipartite structures converge instead of oscillating).
|
|
2005
|
+
* Scores flow along edge direction: a node's score is fed by its incoming
|
|
2006
|
+
* neighbors; undirected edges feed both endpoints. The result vector is
|
|
2007
|
+
* Euclidean (L2) normalized.
|
|
2008
|
+
*
|
|
2009
|
+
* Throws when the iteration has not converged (L1 error < `n × tolerance`)
|
|
2010
|
+
* within `maxIterations`.
|
|
2011
|
+
*/
|
|
2012
|
+
function getEigenvectorCentrality(graph, options) {
|
|
2013
|
+
if (getNodeIds(graph).length === 0) return {};
|
|
2014
|
+
const maxIterations = options?.maxIterations ?? 100;
|
|
2015
|
+
const tolerance = options?.tolerance ?? 1e-6;
|
|
2016
|
+
const getWeight = options?.getWeight;
|
|
2017
|
+
const csr = getCSR(graph);
|
|
2018
|
+
const n = csr.ids.length;
|
|
2019
|
+
let current = new Float64Array(n).fill(1 / n);
|
|
2020
|
+
let converged = false;
|
|
2021
|
+
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
2022
|
+
const next = Float64Array.from(current);
|
|
2023
|
+
for (let w = 0; w < n; w++) for (let a = csr.inOffsets[w]; a < csr.inOffsets[w + 1]; a++) {
|
|
2024
|
+
const weight = getWeight ? getWeight(graph.edges[csr.inEdgeIndex[a]]) : 1;
|
|
2025
|
+
next[w] += current[csr.inOrigins[a]] * weight;
|
|
2026
|
+
}
|
|
2027
|
+
normalizeTypedVector(next);
|
|
2028
|
+
let error = 0;
|
|
2029
|
+
for (let i = 0; i < n; i++) error += Math.abs(next[i] - current[i]);
|
|
2030
|
+
current = next;
|
|
2031
|
+
if (error < n * tolerance) {
|
|
2032
|
+
converged = true;
|
|
2033
|
+
break;
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
if (!converged) throw new Error(`getEigenvectorCentrality: power iteration failed to converge within ${maxIterations} iterations (tolerance ${tolerance}) — increase options.maxIterations or loosen options.tolerance`);
|
|
2037
|
+
const scores = createEmptyScoreMap(graph);
|
|
2038
|
+
for (let i = 0; i < n; i++) scores[csr.ids[i]] = current[i];
|
|
2039
|
+
return scores;
|
|
2040
|
+
}
|
|
2041
|
+
/**
|
|
2042
|
+
* Returns Katz centrality scores for all nodes.
|
|
2043
|
+
*
|
|
2044
|
+
* Iterates `x' = alpha · Aᵀx + beta` to its fixed point (networkx-style),
|
|
2045
|
+
* then Euclidean (L2) normalizes the result. Scores flow along edge
|
|
2046
|
+
* direction: a node's score is fed by its incoming neighbors; undirected
|
|
2047
|
+
* edges feed both endpoints.
|
|
2048
|
+
*
|
|
2049
|
+
* Converges only when `alpha` is below the reciprocal of the largest
|
|
2050
|
+
* eigenvalue of the adjacency matrix; throws when the iteration has not
|
|
2051
|
+
* converged (L1 error < `n × tolerance`) within `maxIterations`.
|
|
2052
|
+
*/
|
|
2053
|
+
function getKatzCentrality(graph, options) {
|
|
2054
|
+
if (getNodeIds(graph).length === 0) return {};
|
|
2055
|
+
const alpha = options?.alpha ?? .1;
|
|
2056
|
+
const beta = options?.beta ?? 1;
|
|
2057
|
+
const maxIterations = options?.maxIterations ?? 100;
|
|
2058
|
+
const tolerance = options?.tolerance ?? 1e-6;
|
|
2059
|
+
const getWeight = options?.getWeight;
|
|
2060
|
+
const csr = getCSR(graph);
|
|
2061
|
+
const n = csr.ids.length;
|
|
2062
|
+
let current = new Float64Array(n);
|
|
2063
|
+
let converged = false;
|
|
2064
|
+
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
2065
|
+
const next = new Float64Array(n).fill(beta);
|
|
2066
|
+
for (let w = 0; w < n; w++) for (let a = csr.inOffsets[w]; a < csr.inOffsets[w + 1]; a++) {
|
|
2067
|
+
const weight = getWeight ? getWeight(graph.edges[csr.inEdgeIndex[a]]) : 1;
|
|
2068
|
+
next[w] += alpha * current[csr.inOrigins[a]] * weight;
|
|
2069
|
+
}
|
|
2070
|
+
let error = 0;
|
|
2071
|
+
for (let i = 0; i < n; i++) error += Math.abs(next[i] - current[i]);
|
|
2072
|
+
current = next;
|
|
2073
|
+
if (error < n * tolerance) {
|
|
2074
|
+
converged = true;
|
|
2075
|
+
break;
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
if (!converged) throw new Error(`getKatzCentrality: iteration failed to converge within ${maxIterations} iterations — alpha ${alpha} may be >= 1/λ_max of the adjacency matrix; decrease options.alpha or increase options.maxIterations`);
|
|
2079
|
+
normalizeTypedVector(current);
|
|
2080
|
+
const scores = createEmptyScoreMap(graph);
|
|
2081
|
+
for (let i = 0; i < n; i++) scores[csr.ids[i]] = current[i];
|
|
2082
|
+
return scores;
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
//#endregion
|
|
2086
|
+
//#region src/algorithms/cores.ts
|
|
2087
|
+
/**
|
|
2088
|
+
* Undirected adjacency over node positions: every edge (regardless of mode
|
|
2089
|
+
* or direction) contributes both arcs, matching the standard k-core
|
|
2090
|
+
* definition. Self-loops are ignored. Positions come from the CSR snapshot
|
|
2091
|
+
* so `ids` order matches `graph.nodes`.
|
|
2092
|
+
*/
|
|
2093
|
+
function buildUndirectedAdjacency(graph) {
|
|
2094
|
+
const csr = getCSR(graph);
|
|
2095
|
+
const n = csr.ids.length;
|
|
2096
|
+
const m = graph.edges.length;
|
|
2097
|
+
const sourcePos = new Int32Array(m);
|
|
2098
|
+
const targetPos = new Int32Array(m);
|
|
2099
|
+
const degree = new Int32Array(n);
|
|
2100
|
+
let arcCount = 0;
|
|
2101
|
+
for (let e = 0; e < m; e++) {
|
|
2102
|
+
const edge = graph.edges[e];
|
|
2103
|
+
const s = csr.indexOf.get(edge.sourceId);
|
|
2104
|
+
const t = csr.indexOf.get(edge.targetId);
|
|
2105
|
+
if (s === t) {
|
|
2106
|
+
sourcePos[e] = -1;
|
|
2107
|
+
continue;
|
|
2108
|
+
}
|
|
2109
|
+
sourcePos[e] = s;
|
|
2110
|
+
targetPos[e] = t;
|
|
2111
|
+
degree[s]++;
|
|
2112
|
+
degree[t]++;
|
|
2113
|
+
arcCount += 2;
|
|
2114
|
+
}
|
|
2115
|
+
const offsets = new Int32Array(n + 1);
|
|
2116
|
+
for (let i = 0; i < n; i++) offsets[i + 1] = offsets[i] + degree[i];
|
|
2117
|
+
const targets = new Int32Array(arcCount);
|
|
2118
|
+
const cursor = Int32Array.from(offsets.subarray(0, n));
|
|
2119
|
+
for (let e = 0; e < m; e++) {
|
|
2120
|
+
if (sourcePos[e] === -1) continue;
|
|
2121
|
+
targets[cursor[sourcePos[e]]++] = targetPos[e];
|
|
2122
|
+
targets[cursor[targetPos[e]]++] = sourcePos[e];
|
|
2123
|
+
}
|
|
2124
|
+
return {
|
|
2125
|
+
ids: csr.ids,
|
|
2126
|
+
offsets,
|
|
2127
|
+
targets
|
|
2128
|
+
};
|
|
2129
|
+
}
|
|
2130
|
+
/**
|
|
2131
|
+
* Returns the core number of every node (largest `k` such that the node
|
|
2132
|
+
* belongs to the k-core).
|
|
2133
|
+
*
|
|
2134
|
+
* Uses Batagelj–Zaveršnik bucket peeling — O(m). Edges are treated as
|
|
2135
|
+
* undirected (the standard k-core definition); self-loops are ignored.
|
|
2136
|
+
*
|
|
2137
|
+
* @example
|
|
2138
|
+
* ```ts
|
|
2139
|
+
* const cores = getCoreNumbers(graph);
|
|
2140
|
+
* console.log(cores.a); // 3
|
|
2141
|
+
* ```
|
|
2142
|
+
*/
|
|
2143
|
+
function getCoreNumbers(graph) {
|
|
2144
|
+
const { ids, offsets, targets } = buildUndirectedAdjacency(graph);
|
|
2145
|
+
const n = ids.length;
|
|
2146
|
+
const core = new Int32Array(n);
|
|
2147
|
+
let maxDegree = 0;
|
|
2148
|
+
for (let i = 0; i < n; i++) {
|
|
2149
|
+
core[i] = offsets[i + 1] - offsets[i];
|
|
2150
|
+
if (core[i] > maxDegree) maxDegree = core[i];
|
|
2151
|
+
}
|
|
2152
|
+
const bin = new Int32Array(maxDegree + 1);
|
|
2153
|
+
for (let i = 0; i < n; i++) bin[core[i]]++;
|
|
2154
|
+
let start = 0;
|
|
2155
|
+
for (let d = 0; d <= maxDegree; d++) {
|
|
2156
|
+
const count = bin[d];
|
|
2157
|
+
bin[d] = start;
|
|
2158
|
+
start += count;
|
|
2159
|
+
}
|
|
2160
|
+
const vert = new Int32Array(n);
|
|
2161
|
+
const pos = new Int32Array(n);
|
|
2162
|
+
for (let i = 0; i < n; i++) {
|
|
2163
|
+
pos[i] = bin[core[i]];
|
|
2164
|
+
vert[pos[i]] = i;
|
|
2165
|
+
bin[core[i]]++;
|
|
2166
|
+
}
|
|
2167
|
+
for (let d = maxDegree; d > 0; d--) bin[d] = bin[d - 1];
|
|
2168
|
+
bin[0] = 0;
|
|
2169
|
+
for (let k = 0; k < n; k++) {
|
|
2170
|
+
const v = vert[k];
|
|
2171
|
+
for (let a = offsets[v]; a < offsets[v + 1]; a++) {
|
|
2172
|
+
const u = targets[a];
|
|
2173
|
+
if (core[u] > core[v]) {
|
|
2174
|
+
const du = core[u];
|
|
2175
|
+
const pu = pos[u];
|
|
2176
|
+
const pw = bin[du];
|
|
2177
|
+
const w = vert[pw];
|
|
2178
|
+
if (u !== w) {
|
|
2179
|
+
vert[pu] = w;
|
|
2180
|
+
pos[w] = pu;
|
|
2181
|
+
vert[pw] = u;
|
|
2182
|
+
pos[u] = pw;
|
|
2183
|
+
}
|
|
2184
|
+
bin[du]++;
|
|
2185
|
+
core[u]--;
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
const result = {};
|
|
2190
|
+
for (let i = 0; i < n; i++) result[ids[i]] = core[i];
|
|
2191
|
+
return result;
|
|
2192
|
+
}
|
|
2193
|
+
/**
|
|
2194
|
+
* Returns the ids of all nodes in the k-core: the maximal subgraph in which
|
|
2195
|
+
* every node has at least `k` neighbors (edges treated as undirected).
|
|
2196
|
+
*
|
|
2197
|
+
* Node order follows `graph.nodes`. `k <= 0` returns every node id.
|
|
2198
|
+
*/
|
|
2199
|
+
function getKCore(graph, k) {
|
|
2200
|
+
const cores = getCoreNumbers(graph);
|
|
2201
|
+
return graph.nodes.filter((node) => cores[node.id] >= k).map((node) => node.id);
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
//#endregion
|
|
2205
|
+
//#region src/algorithms/bipartite.ts
|
|
2206
|
+
/**
|
|
2207
|
+
* BFS 2-coloring over undirected adjacency. Edges are treated as undirected
|
|
2208
|
+
* regardless of mode/direction. Returns either the coloring or the edge that
|
|
2209
|
+
* proves the graph is not bipartite.
|
|
2210
|
+
*/
|
|
2211
|
+
function getTwoColoring(graph) {
|
|
2212
|
+
const csr = getCSR(graph);
|
|
2213
|
+
const n = csr.ids.length;
|
|
2214
|
+
const m = graph.edges.length;
|
|
2215
|
+
const degree = new Int32Array(n);
|
|
2216
|
+
for (let e = 0; e < m; e++) {
|
|
2217
|
+
const edge = graph.edges[e];
|
|
2218
|
+
if (edge.sourceId === edge.targetId) return { conflictEdgeId: edge.id };
|
|
2219
|
+
degree[csr.indexOf.get(edge.sourceId)]++;
|
|
2220
|
+
degree[csr.indexOf.get(edge.targetId)]++;
|
|
2221
|
+
}
|
|
2222
|
+
const offsets = new Int32Array(n + 1);
|
|
2223
|
+
for (let i = 0; i < n; i++) offsets[i + 1] = offsets[i] + degree[i];
|
|
2224
|
+
const targets = new Int32Array(offsets[n]);
|
|
2225
|
+
const arcEdge = new Int32Array(offsets[n]);
|
|
2226
|
+
const cursor = Int32Array.from(offsets.subarray(0, n));
|
|
2227
|
+
for (let e = 0; e < m; e++) {
|
|
2228
|
+
const edge = graph.edges[e];
|
|
2229
|
+
const s = csr.indexOf.get(edge.sourceId);
|
|
2230
|
+
const t = csr.indexOf.get(edge.targetId);
|
|
2231
|
+
targets[cursor[s]] = t;
|
|
2232
|
+
arcEdge[cursor[s]++] = e;
|
|
2233
|
+
targets[cursor[t]] = s;
|
|
2234
|
+
arcEdge[cursor[t]++] = e;
|
|
2235
|
+
}
|
|
2236
|
+
const colors = new Int8Array(n).fill(-1);
|
|
2237
|
+
const queue = new Int32Array(n);
|
|
2238
|
+
for (let root = 0; root < n; root++) {
|
|
2239
|
+
if (colors[root] !== -1) continue;
|
|
2240
|
+
colors[root] = 0;
|
|
2241
|
+
queue[0] = root;
|
|
2242
|
+
let head = 0;
|
|
2243
|
+
let tail = 1;
|
|
2244
|
+
while (head < tail) {
|
|
2245
|
+
const u = queue[head++];
|
|
2246
|
+
for (let a = offsets[u]; a < offsets[u + 1]; a++) {
|
|
2247
|
+
const v = targets[a];
|
|
2248
|
+
if (colors[v] === -1) {
|
|
2249
|
+
colors[v] = 1 - colors[u];
|
|
2250
|
+
queue[tail++] = v;
|
|
2251
|
+
} else if (colors[v] === colors[u]) return { conflictEdgeId: graph.edges[arcEdge[a]].id };
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
return { colors };
|
|
2256
|
+
}
|
|
2257
|
+
/**
|
|
2258
|
+
* Returns whether the graph is bipartite (2-colorable).
|
|
2259
|
+
*
|
|
2260
|
+
* Edges are treated as undirected; self-loops make a graph non-bipartite.
|
|
2261
|
+
* Runs a BFS 2-coloring per connected component — O(n + m).
|
|
2262
|
+
*/
|
|
2263
|
+
function isBipartite(graph) {
|
|
2264
|
+
return "colors" in getTwoColoring(graph);
|
|
2265
|
+
}
|
|
2266
|
+
/**
|
|
2267
|
+
* Returns a maximum-cardinality matching of a bipartite graph using
|
|
2268
|
+
* Hopcroft–Karp — O(m·√n).
|
|
2269
|
+
*
|
|
2270
|
+
* The bipartition is derived by 2-coloring (edges treated as undirected).
|
|
2271
|
+
* Each match reports the realizing edge with its stored `sourceId`/
|
|
2272
|
+
* `targetId` orientation; for parallel edges between a matched pair, the
|
|
2273
|
+
* first edge used by the algorithm is reported.
|
|
2274
|
+
*
|
|
2275
|
+
* Throws if the graph is not bipartite, naming the offending edge.
|
|
2276
|
+
*/
|
|
2277
|
+
function getMaximumBipartiteMatching(graph) {
|
|
2278
|
+
const coloring = getTwoColoring(graph);
|
|
2279
|
+
if (!("colors" in coloring)) throw new Error(`getMaximumBipartiteMatching: graph is not bipartite — edge "${coloring.conflictEdgeId}" closes an odd cycle (or is a self-loop); a maximum bipartite matching requires a bipartite graph, check with isBipartite() first`);
|
|
2280
|
+
const { colors } = coloring;
|
|
2281
|
+
const csr = getCSR(graph);
|
|
2282
|
+
const n = csr.ids.length;
|
|
2283
|
+
const m = graph.edges.length;
|
|
2284
|
+
const degree = new Int32Array(n);
|
|
2285
|
+
for (let e = 0; e < m; e++) {
|
|
2286
|
+
const edge = graph.edges[e];
|
|
2287
|
+
const s = csr.indexOf.get(edge.sourceId);
|
|
2288
|
+
const t = csr.indexOf.get(edge.targetId);
|
|
2289
|
+
degree[colors[s] === 0 ? s : t]++;
|
|
2290
|
+
}
|
|
2291
|
+
const offsets = new Int32Array(n + 1);
|
|
2292
|
+
for (let i = 0; i < n; i++) offsets[i + 1] = offsets[i] + degree[i];
|
|
2293
|
+
const targets = new Int32Array(offsets[n]);
|
|
2294
|
+
const arcEdge = new Int32Array(offsets[n]);
|
|
2295
|
+
const cursor = Int32Array.from(offsets.subarray(0, n));
|
|
2296
|
+
for (let e = 0; e < m; e++) {
|
|
2297
|
+
const edge = graph.edges[e];
|
|
2298
|
+
const s = csr.indexOf.get(edge.sourceId);
|
|
2299
|
+
const t = csr.indexOf.get(edge.targetId);
|
|
2300
|
+
const left = colors[s] === 0 ? s : t;
|
|
2301
|
+
const right = colors[s] === 0 ? t : s;
|
|
2302
|
+
targets[cursor[left]] = right;
|
|
2303
|
+
arcEdge[cursor[left]++] = e;
|
|
2304
|
+
}
|
|
2305
|
+
const INF = 2147483647;
|
|
2306
|
+
const matchLeft = new Int32Array(n).fill(-1);
|
|
2307
|
+
const matchRight = new Int32Array(n).fill(-1);
|
|
2308
|
+
const matchEdge = new Int32Array(n).fill(-1);
|
|
2309
|
+
const dist = new Int32Array(n);
|
|
2310
|
+
const queue = new Int32Array(n);
|
|
2311
|
+
function hasAugmentingLayer() {
|
|
2312
|
+
let head = 0;
|
|
2313
|
+
let tail = 0;
|
|
2314
|
+
for (let u = 0; u < n; u++) {
|
|
2315
|
+
if (colors[u] !== 0) continue;
|
|
2316
|
+
if (matchLeft[u] === -1) {
|
|
2317
|
+
dist[u] = 0;
|
|
2318
|
+
queue[tail++] = u;
|
|
2319
|
+
} else dist[u] = INF;
|
|
2320
|
+
}
|
|
2321
|
+
let foundAugmenting = false;
|
|
2322
|
+
while (head < tail) {
|
|
2323
|
+
const u = queue[head++];
|
|
2324
|
+
for (let a = offsets[u]; a < offsets[u + 1]; a++) {
|
|
2325
|
+
const w = matchRight[targets[a]];
|
|
2326
|
+
if (w === -1) foundAugmenting = true;
|
|
2327
|
+
else if (dist[w] === INF) {
|
|
2328
|
+
dist[w] = dist[u] + 1;
|
|
2329
|
+
queue[tail++] = w;
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
return foundAugmenting;
|
|
2334
|
+
}
|
|
2335
|
+
function hasAugmentedMatchFrom(u) {
|
|
2336
|
+
for (let a = offsets[u]; a < offsets[u + 1]; a++) {
|
|
2337
|
+
const v = targets[a];
|
|
2338
|
+
const w = matchRight[v];
|
|
2339
|
+
if (w === -1 || dist[w] === dist[u] + 1 && hasAugmentedMatchFrom(w)) {
|
|
2340
|
+
matchLeft[u] = v;
|
|
2341
|
+
matchRight[v] = u;
|
|
2342
|
+
matchEdge[u] = arcEdge[a];
|
|
2343
|
+
return true;
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
dist[u] = INF;
|
|
2347
|
+
return false;
|
|
2348
|
+
}
|
|
2349
|
+
while (hasAugmentingLayer()) for (let u = 0; u < n; u++) if (colors[u] === 0 && matchLeft[u] === -1) hasAugmentedMatchFrom(u);
|
|
2350
|
+
const matches = [];
|
|
2351
|
+
for (let u = 0; u < n; u++) {
|
|
2352
|
+
if (matchEdge[u] === -1) continue;
|
|
2353
|
+
const edge = graph.edges[matchEdge[u]];
|
|
2354
|
+
matches.push({
|
|
2355
|
+
sourceId: edge.sourceId,
|
|
2356
|
+
targetId: edge.targetId,
|
|
2357
|
+
edgeId: edge.id
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
return matches;
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
//#endregion
|
|
2364
|
+
//#region src/algorithms/community.ts
|
|
2365
|
+
function getUndirectedNeighbors$1(graph, nodeId) {
|
|
2366
|
+
const idx = getIndex(graph);
|
|
2367
|
+
const neighbors = [];
|
|
2368
|
+
for (const edgeId of idx.outEdges.get(nodeId) ?? []) {
|
|
2369
|
+
const edgeIndex = idx.edgeById.get(edgeId);
|
|
2370
|
+
if (edgeIndex !== void 0) neighbors.push({
|
|
2371
|
+
nodeId: graph.edges[edgeIndex].targetId,
|
|
2372
|
+
edgeId
|
|
2373
|
+
});
|
|
2374
|
+
}
|
|
2375
|
+
for (const edgeId of idx.inEdges.get(nodeId) ?? []) {
|
|
2376
|
+
const edgeIndex = idx.edgeById.get(edgeId);
|
|
2377
|
+
if (edgeIndex !== void 0) neighbors.push({
|
|
2378
|
+
nodeId: graph.edges[edgeIndex].sourceId,
|
|
2379
|
+
edgeId
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
2382
|
+
return neighbors;
|
|
2383
|
+
}
|
|
2384
|
+
function getUndirectedConnectedComponents(graph) {
|
|
2385
|
+
const idx = getIndex(graph);
|
|
2386
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2387
|
+
const communities = [];
|
|
2388
|
+
for (const node of graph.nodes) {
|
|
2389
|
+
if (visited.has(node.id)) continue;
|
|
2390
|
+
const community = [];
|
|
2391
|
+
const queue = [node.id];
|
|
2392
|
+
visited.add(node.id);
|
|
2393
|
+
while (queue.length > 0) {
|
|
2394
|
+
const currentId = queue.shift();
|
|
2395
|
+
const nodeIndex = idx.nodeById.get(currentId);
|
|
2396
|
+
if (nodeIndex !== void 0) community.push(graph.nodes[nodeIndex]);
|
|
2397
|
+
for (const neighbor of getUndirectedNeighbors$1(graph, currentId)) {
|
|
2398
|
+
if (visited.has(neighbor.nodeId)) continue;
|
|
2399
|
+
visited.add(neighbor.nodeId);
|
|
2400
|
+
queue.push(neighbor.nodeId);
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
communities.push(community.sort((a, b) => a.id.localeCompare(b.id)));
|
|
2404
|
+
}
|
|
2405
|
+
return communities.sort((a, b) => a[0].id.localeCompare(b[0].id));
|
|
2406
|
+
}
|
|
2407
|
+
function getNodeMap(graph) {
|
|
2408
|
+
return new Map(graph.nodes.map((node) => [node.id, node]));
|
|
2409
|
+
}
|
|
2410
|
+
function normalizeCommunities(graph, labels) {
|
|
2411
|
+
const nodeMap = getNodeMap(graph);
|
|
2412
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
2413
|
+
for (const [nodeId, label] of Object.entries(labels)) {
|
|
2414
|
+
if (!grouped.has(label)) grouped.set(label, []);
|
|
2415
|
+
const node = nodeMap.get(nodeId);
|
|
2416
|
+
if (node) grouped.get(label).push(node);
|
|
2417
|
+
}
|
|
2418
|
+
return [...grouped.values()].map((community) => community.sort((a, b) => a.id.localeCompare(b.id))).sort((a, b) => a[0].id.localeCompare(b[0].id));
|
|
2419
|
+
}
|
|
2420
|
+
function getEdgeBetweenness(graph) {
|
|
2421
|
+
const scores = Object.fromEntries(graph.edges.map((edge) => [edge.id, 0]));
|
|
2422
|
+
for (const source of graph.nodes) {
|
|
2423
|
+
const stack = [];
|
|
2424
|
+
const predecessors = /* @__PURE__ */ new Map();
|
|
2425
|
+
const sigma = /* @__PURE__ */ new Map();
|
|
2426
|
+
const distance = /* @__PURE__ */ new Map();
|
|
2427
|
+
const queue = [source.id];
|
|
2428
|
+
for (const node of graph.nodes) {
|
|
2429
|
+
predecessors.set(node.id, []);
|
|
2430
|
+
sigma.set(node.id, 0);
|
|
2431
|
+
distance.set(node.id, -1);
|
|
2432
|
+
}
|
|
2433
|
+
sigma.set(source.id, 1);
|
|
2434
|
+
distance.set(source.id, 0);
|
|
2435
|
+
while (queue.length > 0) {
|
|
2436
|
+
const currentId = queue.shift();
|
|
2437
|
+
stack.push(currentId);
|
|
2438
|
+
for (const neighbor of getUndirectedNeighbors$1(graph, currentId)) {
|
|
2439
|
+
if (distance.get(neighbor.nodeId) === -1) {
|
|
2440
|
+
queue.push(neighbor.nodeId);
|
|
2441
|
+
distance.set(neighbor.nodeId, distance.get(currentId) + 1);
|
|
2442
|
+
}
|
|
2443
|
+
if (distance.get(neighbor.nodeId) === distance.get(currentId) + 1) {
|
|
2444
|
+
sigma.set(neighbor.nodeId, sigma.get(neighbor.nodeId) + sigma.get(currentId));
|
|
2445
|
+
predecessors.get(neighbor.nodeId).push({
|
|
2446
|
+
nodeId: currentId,
|
|
2447
|
+
edgeId: neighbor.edgeId
|
|
2448
|
+
});
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
const delta = /* @__PURE__ */ new Map();
|
|
2453
|
+
for (const node of graph.nodes) delta.set(node.id, 0);
|
|
2454
|
+
while (stack.length > 0) {
|
|
2455
|
+
const nodeId = stack.pop();
|
|
2456
|
+
const sigmaNode = sigma.get(nodeId);
|
|
2457
|
+
if (sigmaNode === 0) continue;
|
|
2458
|
+
for (const predecessor of predecessors.get(nodeId)) {
|
|
2459
|
+
const contribution = sigma.get(predecessor.nodeId) / sigmaNode * (1 + delta.get(nodeId));
|
|
2460
|
+
scores[predecessor.edgeId] += contribution;
|
|
2461
|
+
delta.set(predecessor.nodeId, delta.get(predecessor.nodeId) + contribution);
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
for (const edgeId of Object.keys(scores)) scores[edgeId] /= 2;
|
|
2466
|
+
return scores;
|
|
2467
|
+
}
|
|
2468
|
+
function cloneWithEdges(graph, edges) {
|
|
2469
|
+
return {
|
|
2470
|
+
...graph,
|
|
2471
|
+
nodes: [...graph.nodes],
|
|
2472
|
+
edges
|
|
2473
|
+
};
|
|
2474
|
+
}
|
|
2475
|
+
function toCommunityIds(communities) {
|
|
2476
|
+
return communities.map((community) => new Set(community.map((node) => node.id)));
|
|
2477
|
+
}
|
|
2478
|
+
/**
|
|
2479
|
+
* Asynchronous LPA: labels update in place as the (seeded-shuffled) round
|
|
2480
|
+
* proceeds; ties among maximal neighbor labels break uniformly at random,
|
|
2481
|
+
* keeping the current label when it is already maximal so rounds terminate.
|
|
2482
|
+
* Deterministic per seed.
|
|
2483
|
+
*/
|
|
2484
|
+
function getSeededLabelPropagation(graph, seed, maxIterations) {
|
|
2485
|
+
const rng = mulberry32(seed);
|
|
2486
|
+
const labels = Object.fromEntries(graph.nodes.map((node) => [node.id, node.id]));
|
|
2487
|
+
const order = graph.nodes.map((node) => node.id);
|
|
2488
|
+
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
2489
|
+
for (let i = order.length - 1; i > 0; i--) {
|
|
2490
|
+
const j = Math.floor(rng() * (i + 1));
|
|
2491
|
+
[order[i], order[j]] = [order[j], order[i]];
|
|
2492
|
+
}
|
|
2493
|
+
let changed = false;
|
|
2494
|
+
for (const nodeId of order) {
|
|
2495
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2496
|
+
for (const neighbor of getUndirectedNeighbors$1(graph, nodeId)) {
|
|
2497
|
+
const label = labels[neighbor.nodeId];
|
|
2498
|
+
counts.set(label, (counts.get(label) ?? 0) + 1);
|
|
2499
|
+
}
|
|
2500
|
+
if (counts.size === 0) continue;
|
|
2501
|
+
let maxCount = 0;
|
|
2502
|
+
for (const count of counts.values()) if (count > maxCount) maxCount = count;
|
|
2503
|
+
const best = [];
|
|
2504
|
+
for (const [label, count] of counts) if (count === maxCount) best.push(label);
|
|
2505
|
+
if (best.includes(labels[nodeId])) continue;
|
|
2506
|
+
labels[nodeId] = best[Math.floor(rng() * best.length)];
|
|
2507
|
+
changed = true;
|
|
2508
|
+
}
|
|
2509
|
+
if (!changed) break;
|
|
2510
|
+
}
|
|
2511
|
+
return normalizeCommunities(graph, labels);
|
|
2512
|
+
}
|
|
2513
|
+
/**
|
|
2514
|
+
* Returns label-propagation communities for the graph.
|
|
2515
|
+
*
|
|
2516
|
+
* The implementation is deterministic: ties are broken by lexicographic label
|
|
2517
|
+
* order so test results remain stable. Pass `options.seed` for the classic
|
|
2518
|
+
* asynchronous variant (shuffled node order per round, random tie-breaking)
|
|
2519
|
+
* — still deterministic per seed.
|
|
2520
|
+
*/
|
|
2521
|
+
function getLabelPropagationCommunities(graph, options) {
|
|
2522
|
+
if (graph.nodes.length === 0) return [];
|
|
2523
|
+
const maxIterations = options?.maxIterations ?? 50;
|
|
2524
|
+
if (options?.seed !== void 0) return getSeededLabelPropagation(graph, options.seed, maxIterations);
|
|
2525
|
+
let labels = Object.fromEntries(graph.nodes.map((node) => [node.id, node.id]));
|
|
2526
|
+
const nodeIds = graph.nodes.map((node) => node.id).sort();
|
|
2527
|
+
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
2528
|
+
const nextLabels = { ...labels };
|
|
2529
|
+
let changed = false;
|
|
2530
|
+
for (const nodeId of nodeIds) {
|
|
2531
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2532
|
+
for (const neighbor of getUndirectedNeighbors$1(graph, nodeId)) {
|
|
2533
|
+
const label = labels[neighbor.nodeId];
|
|
2534
|
+
counts.set(label, (counts.get(label) ?? 0) + 1);
|
|
2535
|
+
}
|
|
2536
|
+
if (counts.size === 0) continue;
|
|
2537
|
+
const bestLabel = [...counts.entries()].sort((a, b) => {
|
|
2538
|
+
if (b[1] !== a[1]) return b[1] - a[1];
|
|
2539
|
+
return a[0].localeCompare(b[0]);
|
|
2540
|
+
})[0][0];
|
|
2541
|
+
if (bestLabel !== labels[nodeId]) {
|
|
2542
|
+
nextLabels[nodeId] = bestLabel;
|
|
2543
|
+
changed = true;
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
labels = nextLabels;
|
|
2547
|
+
if (!changed) break;
|
|
2548
|
+
}
|
|
2549
|
+
return normalizeCommunities(graph, labels);
|
|
2550
|
+
}
|
|
2551
|
+
/**
|
|
2552
|
+
* Lazily yields Girvan-Newman community splits as edge betweenness removes
|
|
2553
|
+
* bridge-like edges from the graph.
|
|
2554
|
+
*/
|
|
2555
|
+
function* genGirvanNewmanCommunities(graph, options) {
|
|
2556
|
+
if (graph.nodes.length === 0 || graph.edges.length === 0) return;
|
|
2557
|
+
const maxLevels = options?.maxLevels ?? Number.POSITIVE_INFINITY;
|
|
2558
|
+
let yielded = 0;
|
|
2559
|
+
let edges = [...graph.edges];
|
|
2560
|
+
let previousCount = getUndirectedConnectedComponents(graph).length;
|
|
2561
|
+
while (edges.length > 0 && yielded < maxLevels) {
|
|
2562
|
+
const betweenness = getEdgeBetweenness(cloneWithEdges(graph, edges));
|
|
2563
|
+
const maxScore = Math.max(...Object.values(betweenness));
|
|
2564
|
+
edges = edges.filter((edge) => betweenness[edge.id] < maxScore - 1e-12);
|
|
2565
|
+
const components = getUndirectedConnectedComponents(cloneWithEdges(graph, edges));
|
|
2566
|
+
if (components.length > previousCount) {
|
|
2567
|
+
yield components;
|
|
2568
|
+
yielded++;
|
|
2569
|
+
previousCount = components.length;
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
/**
|
|
2574
|
+
* Returns the requested Girvan-Newman split level eagerly.
|
|
2575
|
+
*
|
|
2576
|
+
* `level: 1` returns the first split yielded by `genGirvanNewmanCommunities`.
|
|
2577
|
+
*/
|
|
2578
|
+
function getGirvanNewmanCommunities(graph, options) {
|
|
2579
|
+
if (graph.nodes.length === 0) return [];
|
|
2580
|
+
const targetLevel = options?.level ?? 1;
|
|
2581
|
+
if (targetLevel <= 0) return getUndirectedConnectedComponents(graph);
|
|
2582
|
+
let last = getUndirectedConnectedComponents(graph);
|
|
2583
|
+
let level = 0;
|
|
2584
|
+
for (const partition of genGirvanNewmanCommunities(graph, { maxLevels: targetLevel })) {
|
|
2585
|
+
last = partition;
|
|
2586
|
+
level++;
|
|
2587
|
+
if (level >= targetLevel) break;
|
|
2588
|
+
}
|
|
2589
|
+
return last;
|
|
2590
|
+
}
|
|
2591
|
+
/**
|
|
2592
|
+
* Returns the modularity score for a partition of communities.
|
|
2593
|
+
*
|
|
2594
|
+
* Community algorithms in this module treat the graph as undirected.
|
|
2595
|
+
*/
|
|
2596
|
+
function getModularity(graph, communities) {
|
|
2597
|
+
if (graph.edges.length === 0 || communities.length === 0) return 0;
|
|
2598
|
+
const nodeIds = graph.nodes.map((node) => node.id);
|
|
2599
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
2600
|
+
const degree = Object.fromEntries(nodeIds.map((nodeId) => [nodeId, 0]));
|
|
2601
|
+
for (const nodeId of nodeIds) adjacency.set(nodeId, /* @__PURE__ */ new Map());
|
|
2602
|
+
for (const edge of graph.edges) {
|
|
2603
|
+
adjacency.get(edge.sourceId).set(edge.targetId, (adjacency.get(edge.sourceId).get(edge.targetId) ?? 0) + 1);
|
|
2604
|
+
adjacency.get(edge.targetId).set(edge.sourceId, (adjacency.get(edge.targetId).get(edge.sourceId) ?? 0) + 1);
|
|
2605
|
+
degree[edge.sourceId]++;
|
|
2606
|
+
degree[edge.targetId]++;
|
|
2607
|
+
}
|
|
2608
|
+
const m2 = graph.edges.length * 2;
|
|
2609
|
+
let modularity = 0;
|
|
2610
|
+
for (const community of toCommunityIds(communities)) {
|
|
2611
|
+
const ids = [...community];
|
|
2612
|
+
for (const i of ids) for (const j of ids) {
|
|
2613
|
+
const aij = adjacency.get(i).get(j) ?? 0;
|
|
2614
|
+
modularity += aij - degree[i] * degree[j] / m2;
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
return modularity / m2;
|
|
2618
|
+
}
|
|
2619
|
+
/**
|
|
2620
|
+
* Returns communities found by greedily merging partitions that improve
|
|
2621
|
+
* modularity the most at each step.
|
|
2622
|
+
*/
|
|
2623
|
+
function getGreedyModularityCommunities(graph) {
|
|
2624
|
+
if (graph.nodes.length === 0) return [];
|
|
2625
|
+
let communities = graph.nodes.map((node) => [node]);
|
|
2626
|
+
let currentScore = getModularity(graph, communities);
|
|
2627
|
+
while (communities.length > 1) {
|
|
2628
|
+
let bestScore = currentScore;
|
|
2629
|
+
let bestMerge;
|
|
2630
|
+
for (let i = 0; i < communities.length; i++) for (let j = i + 1; j < communities.length; j++) {
|
|
2631
|
+
const merged = communities.filter((_, index) => index !== i && index !== j);
|
|
2632
|
+
merged.push([...communities[i], ...communities[j]].sort((a, b) => a.id.localeCompare(b.id)));
|
|
2633
|
+
const score = getModularity(graph, merged);
|
|
2634
|
+
if (score > bestScore + 1e-12) {
|
|
2635
|
+
bestScore = score;
|
|
2636
|
+
bestMerge = merged;
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
if (!bestMerge) break;
|
|
2640
|
+
communities = bestMerge.sort((a, b) => a[0].id.localeCompare(b[0].id));
|
|
2641
|
+
currentScore = bestScore;
|
|
2642
|
+
}
|
|
2643
|
+
return communities;
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
//#endregion
|
|
2647
|
+
//#region src/algorithms/connectivity.ts
|
|
2648
|
+
function getUndirectedNeighbors(graph, nodeId) {
|
|
2649
|
+
const idx = getIndex(graph);
|
|
2650
|
+
const neighbors = [];
|
|
2651
|
+
for (const edgeId of idx.outEdges.get(nodeId) ?? []) {
|
|
2652
|
+
const edgeIndex = idx.edgeById.get(edgeId);
|
|
2653
|
+
if (edgeIndex !== void 0) neighbors.push({
|
|
2654
|
+
nodeId: graph.edges[edgeIndex].targetId,
|
|
2655
|
+
edgeId
|
|
2656
|
+
});
|
|
2657
|
+
}
|
|
2658
|
+
for (const edgeId of idx.inEdges.get(nodeId) ?? []) {
|
|
2659
|
+
const edgeIndex = idx.edgeById.get(edgeId);
|
|
2660
|
+
if (edgeIndex !== void 0) neighbors.push({
|
|
2661
|
+
nodeId: graph.edges[edgeIndex].sourceId,
|
|
2662
|
+
edgeId
|
|
2663
|
+
});
|
|
2664
|
+
}
|
|
2665
|
+
return neighbors;
|
|
2666
|
+
}
|
|
2667
|
+
function popComponentUntil(state, stopEdgeId) {
|
|
2668
|
+
const nodeIds = /* @__PURE__ */ new Set();
|
|
2669
|
+
while (state.edgeStack.length > 0) {
|
|
2670
|
+
const edgeId = state.edgeStack.pop();
|
|
2671
|
+
const edge = state.edgeById.get(edgeId);
|
|
2672
|
+
if (edge) {
|
|
2673
|
+
nodeIds.add(edge.sourceId);
|
|
2674
|
+
nodeIds.add(edge.targetId);
|
|
2675
|
+
}
|
|
2676
|
+
if (edgeId === stopEdgeId) break;
|
|
2677
|
+
}
|
|
2678
|
+
if (nodeIds.size > 0) state.components.push(nodeIds);
|
|
2679
|
+
}
|
|
2680
|
+
function finalizeRemainingComponent(state) {
|
|
2681
|
+
if (state.edgeStack.length === 0) return;
|
|
2682
|
+
const nodeIds = /* @__PURE__ */ new Set();
|
|
2683
|
+
while (state.edgeStack.length > 0) {
|
|
2684
|
+
const edge = state.edgeById.get(state.edgeStack.pop());
|
|
2685
|
+
if (edge) {
|
|
2686
|
+
nodeIds.add(edge.sourceId);
|
|
2687
|
+
nodeIds.add(edge.targetId);
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
if (nodeIds.size > 0) state.components.push(nodeIds);
|
|
2691
|
+
}
|
|
2692
|
+
function traverseConnectivity(graph, nodeId, parentEdgeId, state) {
|
|
2693
|
+
state.time += 1;
|
|
2694
|
+
state.disc.set(nodeId, state.time);
|
|
2695
|
+
state.low.set(nodeId, state.time);
|
|
2696
|
+
let childCount = 0;
|
|
2697
|
+
for (const neighbor of getUndirectedNeighbors(graph, nodeId)) {
|
|
2698
|
+
if (neighbor.edgeId === parentEdgeId) continue;
|
|
2699
|
+
if (!state.disc.has(neighbor.nodeId)) {
|
|
2700
|
+
childCount += 1;
|
|
2701
|
+
state.edgeStack.push(neighbor.edgeId);
|
|
2702
|
+
traverseConnectivity(graph, neighbor.nodeId, neighbor.edgeId, state);
|
|
2703
|
+
state.low.set(nodeId, Math.min(state.low.get(nodeId), state.low.get(neighbor.nodeId)));
|
|
2704
|
+
if (state.low.get(neighbor.nodeId) > state.disc.get(nodeId)) state.bridges.add(neighbor.edgeId);
|
|
2705
|
+
if (state.low.get(neighbor.nodeId) >= state.disc.get(nodeId)) {
|
|
2706
|
+
if (parentEdgeId !== null) state.articulationPoints.add(nodeId);
|
|
2707
|
+
popComponentUntil(state, neighbor.edgeId);
|
|
2708
|
+
}
|
|
2709
|
+
} else if (state.disc.get(neighbor.nodeId) < state.disc.get(nodeId)) {
|
|
2710
|
+
state.edgeStack.push(neighbor.edgeId);
|
|
2711
|
+
state.low.set(nodeId, Math.min(state.low.get(nodeId), state.disc.get(neighbor.nodeId)));
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
if (parentEdgeId === null && childCount > 1) state.articulationPoints.add(nodeId);
|
|
2715
|
+
}
|
|
2716
|
+
function analyzeConnectivity(graph) {
|
|
2717
|
+
const state = {
|
|
2718
|
+
time: 0,
|
|
2719
|
+
disc: /* @__PURE__ */ new Map(),
|
|
2720
|
+
low: /* @__PURE__ */ new Map(),
|
|
2721
|
+
edgeStack: [],
|
|
2722
|
+
bridges: /* @__PURE__ */ new Set(),
|
|
2723
|
+
articulationPoints: /* @__PURE__ */ new Set(),
|
|
2724
|
+
components: [],
|
|
2725
|
+
nodeById: new Map(graph.nodes.map((node) => [node.id, node])),
|
|
2726
|
+
edgeById: new Map(graph.edges.map((edge) => [edge.id, edge]))
|
|
2727
|
+
};
|
|
2728
|
+
for (const node of graph.nodes) {
|
|
2729
|
+
if (state.disc.has(node.id)) continue;
|
|
2730
|
+
traverseConnectivity(graph, node.id, null, state);
|
|
2731
|
+
finalizeRemainingComponent(state);
|
|
2732
|
+
}
|
|
2733
|
+
return state;
|
|
2734
|
+
}
|
|
2735
|
+
/**
|
|
2736
|
+
* Returns bridge edges whose removal disconnects the graph.
|
|
2737
|
+
*
|
|
2738
|
+
* Connectivity algorithms in this module treat the graph as undirected.
|
|
2739
|
+
*/
|
|
2740
|
+
function getBridges(graph) {
|
|
2741
|
+
if (graph.edges.length === 0) return [];
|
|
2742
|
+
const state = analyzeConnectivity(graph);
|
|
2743
|
+
return [...state.bridges].map((edgeId) => state.edgeById.get(edgeId)).sort((a, b) => a.id.localeCompare(b.id));
|
|
2744
|
+
}
|
|
2745
|
+
/**
|
|
2746
|
+
* Returns articulation points (cut vertices) for the graph.
|
|
2747
|
+
*
|
|
2748
|
+
* Connectivity algorithms in this module treat the graph as undirected.
|
|
2749
|
+
*/
|
|
2750
|
+
function getArticulationPoints(graph) {
|
|
2751
|
+
if (graph.nodes.length === 0) return [];
|
|
2752
|
+
const state = analyzeConnectivity(graph);
|
|
2753
|
+
return [...state.articulationPoints].map((nodeId) => state.nodeById.get(nodeId)).sort((a, b) => a.id.localeCompare(b.id));
|
|
2754
|
+
}
|
|
2755
|
+
/**
|
|
2756
|
+
* Returns biconnected components as arrays of nodes.
|
|
2757
|
+
*
|
|
2758
|
+
* Articulation points may appear in multiple returned components.
|
|
2759
|
+
*/
|
|
2760
|
+
function getBiconnectedComponents(graph) {
|
|
2761
|
+
if (graph.edges.length === 0) return [];
|
|
2762
|
+
const state = analyzeConnectivity(graph);
|
|
2763
|
+
return state.components.map((component) => [...component].map((nodeId) => state.nodeById.get(nodeId)).sort((a, b) => a.id.localeCompare(b.id))).sort((a, b) => a[0].id.localeCompare(b[0].id));
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2766
|
+
//#endregion
|
|
2767
|
+
//#region src/algorithms/isomorphism.ts
|
|
2768
|
+
function getDegreeSignature(graph, nodeId) {
|
|
2769
|
+
const idx = getIndex(graph);
|
|
2770
|
+
let inDegree = 0;
|
|
2771
|
+
let outDegree = 0;
|
|
2772
|
+
let undirected = 0;
|
|
2773
|
+
const countedNonDirected = /* @__PURE__ */ new Set();
|
|
2774
|
+
for (const eid of idx.outEdges.get(nodeId) ?? []) {
|
|
2775
|
+
const edge = graph.edges[idx.edgeById.get(eid)];
|
|
2776
|
+
if (getEdgeMode(graph, edge) === "directed") outDegree++;
|
|
2777
|
+
else if (!countedNonDirected.has(eid)) {
|
|
2778
|
+
countedNonDirected.add(eid);
|
|
2779
|
+
undirected++;
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
for (const eid of idx.inEdges.get(nodeId) ?? []) {
|
|
2783
|
+
const edge = graph.edges[idx.edgeById.get(eid)];
|
|
2784
|
+
if (getEdgeMode(graph, edge) === "directed") inDegree++;
|
|
2785
|
+
else if (!countedNonDirected.has(eid)) {
|
|
2786
|
+
countedNonDirected.add(eid);
|
|
2787
|
+
undirected++;
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
return `d:${inDegree}:${outDegree}:u:${undirected}`;
|
|
2791
|
+
}
|
|
2792
|
+
function getEdgesBetween(graph, sourceId, targetId) {
|
|
2793
|
+
return graph.edges.filter((edge) => {
|
|
2794
|
+
if (edge.sourceId === sourceId && edge.targetId === targetId) return true;
|
|
2795
|
+
return edge.sourceId === targetId && edge.targetId === sourceId && getEdgeMode(graph, edge) !== "directed";
|
|
2796
|
+
});
|
|
2797
|
+
}
|
|
2798
|
+
function edgesAreCompatible(edgesA, edgesB, edgeMatch) {
|
|
2799
|
+
if (edgesA.length !== edgesB.length) return false;
|
|
2800
|
+
if (!edgeMatch || edgesA.length === 0) return true;
|
|
2801
|
+
const remaining = [...edgesB];
|
|
2802
|
+
for (const edgeA of edgesA) {
|
|
2803
|
+
const matchIndex = remaining.findIndex((edgeB) => edgeMatch(edgeA, edgeB));
|
|
2804
|
+
if (matchIndex === -1) return false;
|
|
2805
|
+
remaining.splice(matchIndex, 1);
|
|
2806
|
+
}
|
|
2807
|
+
return true;
|
|
2808
|
+
}
|
|
2809
|
+
/**
|
|
2810
|
+
* Returns whether two graphs are structurally isomorphic.
|
|
2811
|
+
*
|
|
2812
|
+
* Optional `nodeMatch` and `edgeMatch` predicates can refine the match using
|
|
2813
|
+
* node and edge payloads.
|
|
2814
|
+
*/
|
|
2815
|
+
function isIsomorphic(graphA, graphB, options) {
|
|
2816
|
+
if (graphA.nodes.length !== graphB.nodes.length) return false;
|
|
2817
|
+
if (graphA.edges.length !== graphB.edges.length) return false;
|
|
2818
|
+
const nodeMatch = options?.nodeMatch;
|
|
2819
|
+
const edgeMatch = options?.edgeMatch;
|
|
2820
|
+
const nodesA = [...graphA.nodes].sort((a, b) => {
|
|
2821
|
+
const sigDiff = getDegreeSignature(graphA, b.id).localeCompare(getDegreeSignature(graphA, a.id));
|
|
2822
|
+
if (sigDiff !== 0) return sigDiff;
|
|
2823
|
+
return a.id.localeCompare(b.id);
|
|
2824
|
+
});
|
|
2825
|
+
const nodesB = [...graphB.nodes];
|
|
2826
|
+
const signaturesA = nodesA.map((node) => getDegreeSignature(graphA, node.id)).sort();
|
|
2827
|
+
const signaturesB = nodesB.map((node) => getDegreeSignature(graphB, node.id)).sort();
|
|
2828
|
+
if (signaturesA.join("|") !== signaturesB.join("|")) return false;
|
|
2829
|
+
const mapping = /* @__PURE__ */ new Map();
|
|
2830
|
+
const usedB = /* @__PURE__ */ new Set();
|
|
2831
|
+
const backtrack = (index) => {
|
|
2832
|
+
if (index >= nodesA.length) return true;
|
|
2833
|
+
const nodeA = nodesA[index];
|
|
2834
|
+
const signatureA = getDegreeSignature(graphA, nodeA.id);
|
|
2835
|
+
for (const nodeB of nodesB) {
|
|
2836
|
+
if (usedB.has(nodeB.id)) continue;
|
|
2837
|
+
if (getDegreeSignature(graphB, nodeB.id) !== signatureA) continue;
|
|
2838
|
+
if (nodeMatch && !nodeMatch(nodeA, nodeB)) continue;
|
|
2839
|
+
if (!edgesAreCompatible(getEdgesBetween(graphA, nodeA.id, nodeA.id), getEdgesBetween(graphB, nodeB.id, nodeB.id), edgeMatch)) continue;
|
|
2840
|
+
let compatible = true;
|
|
2841
|
+
for (const [mappedAId, mappedBId] of mapping.entries()) {
|
|
2842
|
+
if (!edgesAreCompatible(getEdgesBetween(graphA, nodeA.id, mappedAId), getEdgesBetween(graphB, nodeB.id, mappedBId), edgeMatch)) {
|
|
2843
|
+
compatible = false;
|
|
2844
|
+
break;
|
|
2845
|
+
}
|
|
2846
|
+
if (!edgesAreCompatible(getEdgesBetween(graphA, mappedAId, nodeA.id), getEdgesBetween(graphB, mappedBId, nodeB.id), edgeMatch)) {
|
|
2847
|
+
compatible = false;
|
|
2848
|
+
break;
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
if (!compatible) continue;
|
|
2852
|
+
mapping.set(nodeA.id, nodeB.id);
|
|
2853
|
+
usedB.add(nodeB.id);
|
|
2854
|
+
if (backtrack(index + 1)) return true;
|
|
2855
|
+
mapping.delete(nodeA.id);
|
|
2856
|
+
usedB.delete(nodeB.id);
|
|
2857
|
+
}
|
|
2858
|
+
return false;
|
|
2859
|
+
};
|
|
2860
|
+
return backtrack(0);
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2863
|
+
//#endregion
|
|
2864
|
+
//#region src/algorithms/louvain.ts
|
|
2865
|
+
/**
|
|
2866
|
+
* Returns communities found by the classic two-phase Louvain modularity
|
|
2867
|
+
* optimization (local moving + community aggregation).
|
|
2868
|
+
*
|
|
2869
|
+
* Like the other community algorithms in this library, the graph is treated
|
|
2870
|
+
* as undirected regardless of `graph.mode` or per-edge modes. Parallel edges
|
|
2871
|
+
* have their weights summed; self-loops contribute to a community's internal
|
|
2872
|
+
* weight.
|
|
2873
|
+
*
|
|
2874
|
+
* The implementation is deterministic: nodes are visited in `graph.nodes`
|
|
2875
|
+
* array order and there is no random shuffling, so tie-breaking is
|
|
2876
|
+
* order-dependent but stable across runs.
|
|
2877
|
+
*
|
|
2878
|
+
* Returns communities of node ids, each community sorted lexicographically
|
|
2879
|
+
* and communities sorted by their first id.
|
|
2880
|
+
*
|
|
2881
|
+
* @example
|
|
2882
|
+
* ```ts
|
|
2883
|
+
* const communities = getLouvainCommunities(graph);
|
|
2884
|
+
* // [['a', 'b', 'c'], ['d', 'e', 'f']]
|
|
2885
|
+
* ```
|
|
2886
|
+
*/
|
|
2887
|
+
function getLouvainCommunities(graph, options) {
|
|
2888
|
+
if (graph.nodes.length === 0) return [];
|
|
2889
|
+
const getWeight = options?.getWeight ?? ((edge) => edge.weight ?? 1);
|
|
2890
|
+
const resolution = options?.resolution ?? 1;
|
|
2891
|
+
const maxPasses = options?.maxPasses ?? 10;
|
|
2892
|
+
const nodeIds = graph.nodes.map((node) => node.id);
|
|
2893
|
+
const indexOf = new Map(nodeIds.map((id, i) => [id, i]));
|
|
2894
|
+
let count = nodeIds.length;
|
|
2895
|
+
let links = Array.from({ length: count }, () => /* @__PURE__ */ new Map());
|
|
2896
|
+
let selfLoops = new Array(count).fill(0);
|
|
2897
|
+
for (const edge of graph.edges) {
|
|
2898
|
+
const u = indexOf.get(edge.sourceId);
|
|
2899
|
+
const v = indexOf.get(edge.targetId);
|
|
2900
|
+
if (u === void 0 || v === void 0) continue;
|
|
2901
|
+
const w = getWeight(edge);
|
|
2902
|
+
if (u === v) selfLoops[u] += w;
|
|
2903
|
+
else {
|
|
2904
|
+
links[u].set(v, (links[u].get(v) ?? 0) + w);
|
|
2905
|
+
links[v].set(u, (links[v].get(u) ?? 0) + w);
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2908
|
+
let membership = nodeIds.map((_, i) => i);
|
|
2909
|
+
for (let pass = 0; pass < maxPasses; pass++) {
|
|
2910
|
+
const degree = links.map((neighbors, i) => 2 * selfLoops[i] + [...neighbors.values()].reduce((sum, w) => sum + w, 0));
|
|
2911
|
+
const m2 = degree.reduce((sum, k) => sum + k, 0);
|
|
2912
|
+
if (m2 === 0) break;
|
|
2913
|
+
const communityOf = Array.from({ length: count }, (_, i) => i);
|
|
2914
|
+
const communityTotal = [...degree];
|
|
2915
|
+
let movedAny = false;
|
|
2916
|
+
let movedThisSweep = true;
|
|
2917
|
+
while (movedThisSweep) {
|
|
2918
|
+
movedThisSweep = false;
|
|
2919
|
+
for (let i = 0; i < count; i++) {
|
|
2920
|
+
const current = communityOf[i];
|
|
2921
|
+
const weightTo = /* @__PURE__ */ new Map();
|
|
2922
|
+
for (const [j, w] of links[i]) {
|
|
2923
|
+
const c = communityOf[j];
|
|
2924
|
+
weightTo.set(c, (weightTo.get(c) ?? 0) + w);
|
|
2925
|
+
}
|
|
2926
|
+
communityTotal[current] -= degree[i];
|
|
2927
|
+
const gainOf = (c) => (weightTo.get(c) ?? 0) - resolution * communityTotal[c] * degree[i] / m2;
|
|
2928
|
+
let best = current;
|
|
2929
|
+
let bestGain = gainOf(current);
|
|
2930
|
+
for (const c of weightTo.keys()) {
|
|
2931
|
+
if (c === current) continue;
|
|
2932
|
+
const gain = gainOf(c);
|
|
2933
|
+
if (gain > bestGain + 1e-12) {
|
|
2934
|
+
best = c;
|
|
2935
|
+
bestGain = gain;
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
communityTotal[best] += degree[i];
|
|
2939
|
+
if (best !== current) {
|
|
2940
|
+
communityOf[i] = best;
|
|
2941
|
+
movedThisSweep = true;
|
|
2942
|
+
movedAny = true;
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
if (!movedAny) break;
|
|
2947
|
+
const renumber = /* @__PURE__ */ new Map();
|
|
2948
|
+
for (let i = 0; i < count; i++) {
|
|
2949
|
+
const c = communityOf[i];
|
|
2950
|
+
if (!renumber.has(c)) renumber.set(c, renumber.size);
|
|
2951
|
+
}
|
|
2952
|
+
const nextCount = renumber.size;
|
|
2953
|
+
const nextLinks = Array.from({ length: nextCount }, () => /* @__PURE__ */ new Map());
|
|
2954
|
+
const nextSelfLoops = new Array(nextCount).fill(0);
|
|
2955
|
+
for (let i = 0; i < count; i++) {
|
|
2956
|
+
const ci = renumber.get(communityOf[i]);
|
|
2957
|
+
nextSelfLoops[ci] += selfLoops[i];
|
|
2958
|
+
for (const [j, w] of links[i]) {
|
|
2959
|
+
const cj = renumber.get(communityOf[j]);
|
|
2960
|
+
if (ci === cj) nextSelfLoops[ci] += w / 2;
|
|
2961
|
+
else nextLinks[ci].set(cj, (nextLinks[ci].get(cj) ?? 0) + w);
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
membership = membership.map((c) => renumber.get(communityOf[c]));
|
|
2965
|
+
links = nextLinks;
|
|
2966
|
+
selfLoops = nextSelfLoops;
|
|
2967
|
+
count = nextCount;
|
|
2968
|
+
if (nextCount === 1) break;
|
|
2969
|
+
}
|
|
2970
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
2971
|
+
for (let i = 0; i < nodeIds.length; i++) {
|
|
2972
|
+
const c = membership[i];
|
|
2973
|
+
if (!grouped.has(c)) grouped.set(c, []);
|
|
2974
|
+
grouped.get(c).push(nodeIds[i]);
|
|
2975
|
+
}
|
|
2976
|
+
return [...grouped.values()].map((ids) => ids.sort((a, b) => a.localeCompare(b))).sort((a, b) => a[0].localeCompare(b[0]));
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2979
|
+
//#endregion
|
|
2980
|
+
//#region src/algorithms/flow.ts
|
|
2981
|
+
/**
|
|
2982
|
+
* Shared Edmonds-Karp solver behind {@link getMaxFlow} and {@link getMinCut}.
|
|
2983
|
+
* `caller`/`fromOption`/`toOption` only shape the error messages.
|
|
2984
|
+
*/
|
|
2985
|
+
function solveMaxFlow(graph, caller, fromOption, toOption, from, to, getCapacity) {
|
|
2986
|
+
const idx = getIndex(graph);
|
|
2987
|
+
if (!idx.nodeById.has(from)) throw new Error(`${caller}: source node "${from}" not found in graph — pass an existing node id as ${fromOption}`);
|
|
2988
|
+
if (!idx.nodeById.has(to)) throw new Error(`${caller}: sink node "${to}" not found in graph — pass an existing node id as ${toOption}`);
|
|
2989
|
+
if (from === to) throw new Error(`${caller}: source and sink are both "${from}" — they must be different nodes`);
|
|
2990
|
+
const arcs = [];
|
|
2991
|
+
const outArcs = /* @__PURE__ */ new Map();
|
|
2992
|
+
for (const node of graph.nodes) outArcs.set(node.id, []);
|
|
2993
|
+
function addArc(u, v, capacity, edgeId, sign) {
|
|
2994
|
+
outArcs.get(u).push(arcs.length);
|
|
2995
|
+
arcs.push({
|
|
2996
|
+
to: v,
|
|
2997
|
+
capacity,
|
|
2998
|
+
flow: 0,
|
|
2999
|
+
edgeId,
|
|
3000
|
+
sign
|
|
3001
|
+
});
|
|
3002
|
+
outArcs.get(v).push(arcs.length);
|
|
3003
|
+
arcs.push({
|
|
3004
|
+
to: u,
|
|
3005
|
+
capacity: 0,
|
|
3006
|
+
flow: 0
|
|
3007
|
+
});
|
|
3008
|
+
}
|
|
3009
|
+
for (const edge of graph.edges) {
|
|
3010
|
+
const capacity = getCapacity(edge);
|
|
3011
|
+
if (capacity < 0) throw new Error(`${caller}: edge "${edge.id}" has negative capacity ${capacity} — capacities must be >= 0; fix edge.weight or provide a non-negative getCapacity`);
|
|
3012
|
+
if (edge.sourceId === edge.targetId) continue;
|
|
3013
|
+
addArc(edge.sourceId, edge.targetId, capacity, edge.id, 1);
|
|
3014
|
+
if (getEdgeMode(graph, edge) !== "directed") addArc(edge.targetId, edge.sourceId, capacity, edge.id, -1);
|
|
3015
|
+
}
|
|
3016
|
+
function residual(arcIndex) {
|
|
3017
|
+
return arcs[arcIndex].capacity - arcs[arcIndex].flow;
|
|
3018
|
+
}
|
|
3019
|
+
let value = 0;
|
|
3020
|
+
while (true) {
|
|
3021
|
+
const parentArc = /* @__PURE__ */ new Map();
|
|
3022
|
+
const queue$1 = [from];
|
|
3023
|
+
const visited = new Set([from]);
|
|
3024
|
+
while (queue$1.length > 0 && !visited.has(to)) {
|
|
3025
|
+
const u = queue$1.shift();
|
|
3026
|
+
for (const ai of outArcs.get(u) ?? []) {
|
|
3027
|
+
const arc = arcs[ai];
|
|
3028
|
+
if (residual(ai) > 0 && !visited.has(arc.to)) {
|
|
3029
|
+
visited.add(arc.to);
|
|
3030
|
+
parentArc.set(arc.to, ai);
|
|
3031
|
+
queue$1.push(arc.to);
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
if (!visited.has(to)) break;
|
|
3036
|
+
let bottleneck = Infinity;
|
|
3037
|
+
for (let v = to; v !== from;) {
|
|
3038
|
+
const ai = parentArc.get(v);
|
|
3039
|
+
bottleneck = Math.min(bottleneck, residual(ai));
|
|
3040
|
+
v = arcs[ai ^ 1].to;
|
|
3041
|
+
}
|
|
3042
|
+
if (bottleneck === Infinity || bottleneck <= 0) break;
|
|
3043
|
+
for (let v = to; v !== from;) {
|
|
3044
|
+
const ai = parentArc.get(v);
|
|
3045
|
+
arcs[ai].flow += bottleneck;
|
|
3046
|
+
arcs[ai ^ 1].flow -= bottleneck;
|
|
3047
|
+
v = arcs[ai ^ 1].to;
|
|
3048
|
+
}
|
|
3049
|
+
value += bottleneck;
|
|
3050
|
+
}
|
|
3051
|
+
const flows = Object.fromEntries(graph.edges.map((edge) => [edge.id, 0]));
|
|
3052
|
+
for (const arc of arcs) if (arc.edgeId !== void 0 && arc.flow > 0) flows[arc.edgeId] += arc.sign * arc.flow;
|
|
3053
|
+
const sourceSide = new Set([from]);
|
|
3054
|
+
const queue = [from];
|
|
3055
|
+
while (queue.length > 0) {
|
|
3056
|
+
const u = queue.shift();
|
|
3057
|
+
for (const ai of outArcs.get(u) ?? []) {
|
|
3058
|
+
const arc = arcs[ai];
|
|
3059
|
+
if (residual(ai) > 0 && !sourceSide.has(arc.to)) {
|
|
3060
|
+
sourceSide.add(arc.to);
|
|
3061
|
+
queue.push(arc.to);
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
const cutEdgeIds = /* @__PURE__ */ new Set();
|
|
3066
|
+
for (let ai = 0; ai < arcs.length; ai++) {
|
|
3067
|
+
const arc = arcs[ai];
|
|
3068
|
+
if (arc.edgeId === void 0) continue;
|
|
3069
|
+
const arcFrom = arcs[ai ^ 1].to;
|
|
3070
|
+
if (sourceSide.has(arcFrom) && !sourceSide.has(arc.to)) cutEdgeIds.add(arc.edgeId);
|
|
3071
|
+
}
|
|
3072
|
+
const cutEdges = graph.edges.filter((edge) => cutEdgeIds.has(edge.id));
|
|
3073
|
+
return {
|
|
3074
|
+
value,
|
|
3075
|
+
flows,
|
|
3076
|
+
cutEdges,
|
|
3077
|
+
sourceSide
|
|
3078
|
+
};
|
|
3079
|
+
}
|
|
3080
|
+
/**
|
|
3081
|
+
* Returns the maximum flow from `from` to `to` using the Edmonds-Karp
|
|
3082
|
+
* algorithm (BFS augmenting paths).
|
|
3083
|
+
*
|
|
3084
|
+
* Directed edges carry capacity from source to target only. Edges whose
|
|
3085
|
+
* effective mode is not `'directed'` (undirected/bidirectional) are modeled
|
|
3086
|
+
* as two independent opposite arcs, each with the edge's full capacity.
|
|
3087
|
+
*
|
|
3088
|
+
* The returned `flows` record maps every edge id to its net flow (positive
|
|
3089
|
+
* in the source→target direction). `cutEdges` is a minimum s-t cut: the
|
|
3090
|
+
* edges crossing from the source side to the sink side of the final
|
|
3091
|
+
* residual graph; the sum of their capacities equals `value`.
|
|
3092
|
+
*
|
|
3093
|
+
* @example
|
|
3094
|
+
* ```ts
|
|
3095
|
+
* const { value, cutEdges } = getMaxFlow(graph, { from: 's', to: 't' });
|
|
3096
|
+
* ```
|
|
3097
|
+
*/
|
|
3098
|
+
function getMaxFlow(graph, options) {
|
|
3099
|
+
const getCapacity = options.getCapacity ?? ((edge) => edge.weight ?? 1);
|
|
3100
|
+
const { value, flows, cutEdges } = solveMaxFlow(graph, "getMaxFlow", "options.from", "options.to", options.from, options.to, getCapacity);
|
|
3101
|
+
return {
|
|
3102
|
+
value,
|
|
3103
|
+
flows,
|
|
3104
|
+
cutEdges
|
|
3105
|
+
};
|
|
3106
|
+
}
|
|
3107
|
+
/**
|
|
3108
|
+
* Returns a minimum s-t cut between `source` and `sink` via the max-flow
|
|
3109
|
+
* min-cut theorem: runs the same Edmonds-Karp solver as {@link getMaxFlow},
|
|
3110
|
+
* then splits the nodes by residual reachability from the source.
|
|
3111
|
+
*
|
|
3112
|
+
* `partition.source` holds every node reachable from `source` in the final
|
|
3113
|
+
* residual graph; `partition.sink` holds the rest (both in `graph.nodes`
|
|
3114
|
+
* order). `cutEdges` are the ids of the edges crossing the cut, and their
|
|
3115
|
+
* total capacity equals `value` (the max-flow value).
|
|
3116
|
+
*
|
|
3117
|
+
* @example
|
|
3118
|
+
* ```ts
|
|
3119
|
+
* const { value, cutEdges, partition } = getMinCut(graph, {
|
|
3120
|
+
* source: 's',
|
|
3121
|
+
* sink: 't',
|
|
3122
|
+
* });
|
|
3123
|
+
* ```
|
|
3124
|
+
*/
|
|
3125
|
+
function getMinCut(graph, options) {
|
|
3126
|
+
const getCapacity = options.getCapacity ?? ((edge) => edge.weight ?? 1);
|
|
3127
|
+
const { value, cutEdges, sourceSide } = solveMaxFlow(graph, "getMinCut", "options.source", "options.sink", options.source, options.sink, getCapacity);
|
|
3128
|
+
const sourcePartition = [];
|
|
3129
|
+
const sinkPartition = [];
|
|
3130
|
+
for (const node of graph.nodes) (sourceSide.has(node.id) ? sourcePartition : sinkPartition).push(node.id);
|
|
3131
|
+
return {
|
|
3132
|
+
value,
|
|
3133
|
+
cutEdges: cutEdges.map((edge) => edge.id),
|
|
3134
|
+
partition: {
|
|
3135
|
+
source: sourcePartition,
|
|
3136
|
+
sink: sinkPartition
|
|
3137
|
+
}
|
|
3138
|
+
};
|
|
3139
|
+
}
|
|
3140
|
+
|
|
3141
|
+
//#endregion
|
|
3142
|
+
//#region src/algorithms/dominators.ts
|
|
3143
|
+
/**
|
|
3144
|
+
* Returns the dominator tree of the graph rooted at `from`, computed with
|
|
3145
|
+
* the Cooper–Harvey–Kennedy iterative algorithm.
|
|
3146
|
+
*
|
|
3147
|
+
* Each reachable node maps to its immediate dominator's id; the root maps
|
|
3148
|
+
* to `null`. Unreachable nodes are omitted. Traversal is mode-aware:
|
|
3149
|
+
* undirected/bidirectional edges are traversable both ways.
|
|
3150
|
+
*
|
|
3151
|
+
* For statecharts this answers "which states must every path from the
|
|
3152
|
+
* initial state pass through to reach this state?" — node `d` dominates
|
|
3153
|
+
* node `n` when every path from the initial state to `n` goes through `d`.
|
|
3154
|
+
*
|
|
3155
|
+
* @example
|
|
3156
|
+
* ```ts
|
|
3157
|
+
* // a→b, a→c, b→d, c→d (diamond)
|
|
3158
|
+
* getDominatorTree(graph, { from: 'a' });
|
|
3159
|
+
* // { a: null, b: 'a', c: 'a', d: 'a' }
|
|
3160
|
+
* ```
|
|
3161
|
+
*/
|
|
3162
|
+
function getDominatorTree(graph, options) {
|
|
3163
|
+
const root = resolveFrom(graph, options);
|
|
3164
|
+
if (!getIndex(graph).nodeById.has(root)) throw new Error(`getDominatorTree: root node "${root}" not found in graph — pass an existing node id as options.from`);
|
|
3165
|
+
const postorder = [];
|
|
3166
|
+
const visited = new Set([root]);
|
|
3167
|
+
const stack = [{
|
|
3168
|
+
id: root,
|
|
3169
|
+
neighborIndex: 0
|
|
3170
|
+
}];
|
|
3171
|
+
const successors = /* @__PURE__ */ new Map();
|
|
3172
|
+
function getSuccessors(id) {
|
|
3173
|
+
let succ = successors.get(id);
|
|
3174
|
+
if (!succ) {
|
|
3175
|
+
succ = getNeighborEdges(graph, id).map((entry) => entry.neighborId);
|
|
3176
|
+
successors.set(id, succ);
|
|
3177
|
+
}
|
|
3178
|
+
return succ;
|
|
3179
|
+
}
|
|
3180
|
+
while (stack.length > 0) {
|
|
3181
|
+
const frame = stack[stack.length - 1];
|
|
3182
|
+
const succ = getSuccessors(frame.id);
|
|
3183
|
+
if (frame.neighborIndex < succ.length) {
|
|
3184
|
+
const next = succ[frame.neighborIndex++];
|
|
3185
|
+
if (!visited.has(next)) {
|
|
3186
|
+
visited.add(next);
|
|
3187
|
+
stack.push({
|
|
3188
|
+
id: next,
|
|
3189
|
+
neighborIndex: 0
|
|
3190
|
+
});
|
|
3191
|
+
}
|
|
3192
|
+
} else {
|
|
3193
|
+
postorder.push(frame.id);
|
|
3194
|
+
stack.pop();
|
|
3195
|
+
}
|
|
3196
|
+
}
|
|
3197
|
+
const rpo = [...postorder].reverse();
|
|
3198
|
+
const rpoNumber = new Map(rpo.map((id, i) => [id, i]));
|
|
3199
|
+
const predecessors = new Map(rpo.map((id) => [id, []]));
|
|
3200
|
+
for (const id of rpo) for (const succ of getSuccessors(id)) if (visited.has(succ)) predecessors.get(succ).push(id);
|
|
3201
|
+
const idom = /* @__PURE__ */ new Map();
|
|
3202
|
+
idom.set(root, root);
|
|
3203
|
+
function intersect(a, b) {
|
|
3204
|
+
let f1 = a;
|
|
3205
|
+
let f2 = b;
|
|
3206
|
+
while (f1 !== f2) {
|
|
3207
|
+
while (rpoNumber.get(f1) > rpoNumber.get(f2)) f1 = idom.get(f1);
|
|
3208
|
+
while (rpoNumber.get(f2) > rpoNumber.get(f1)) f2 = idom.get(f2);
|
|
3209
|
+
}
|
|
3210
|
+
return f1;
|
|
3211
|
+
}
|
|
3212
|
+
let changed = true;
|
|
3213
|
+
while (changed) {
|
|
3214
|
+
changed = false;
|
|
3215
|
+
for (const id of rpo) {
|
|
3216
|
+
if (id === root) continue;
|
|
3217
|
+
let newIdom;
|
|
3218
|
+
for (const pred of predecessors.get(id)) {
|
|
3219
|
+
if (!idom.has(pred)) continue;
|
|
3220
|
+
newIdom = newIdom === void 0 ? pred : intersect(pred, newIdom);
|
|
3221
|
+
}
|
|
3222
|
+
if (newIdom !== void 0 && idom.get(id) !== newIdom) {
|
|
3223
|
+
idom.set(id, newIdom);
|
|
3224
|
+
changed = true;
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
const result = {};
|
|
3229
|
+
for (const id of rpo) result[id] = id === root ? null : idom.get(id);
|
|
3230
|
+
return result;
|
|
3231
|
+
}
|
|
3232
|
+
|
|
3233
|
+
//#endregion
|
|
3234
|
+
//#region src/algorithms/reduction.ts
|
|
3235
|
+
/**
|
|
3236
|
+
* Returns a new graph with all transitively-redundant edges removed (the
|
|
3237
|
+
* transitive reduction). The input graph is not mutated; nodes and surviving
|
|
3238
|
+
* edges keep all of their fields.
|
|
3239
|
+
*
|
|
3240
|
+
* An edge u→v is removed when v is also reachable from u via a path of
|
|
3241
|
+
* length ≥ 2. Exact-duplicate parallel edges u→v collapse to the first one
|
|
3242
|
+
* in `graph.edges` order (a duplicate adds no reachability, so at most one
|
|
3243
|
+
* edge per (u, v) pair survives).
|
|
3244
|
+
*
|
|
3245
|
+
* DAG-only: throws when the graph contains a cycle or any edge whose
|
|
3246
|
+
* effective mode is not `'directed'`.
|
|
3247
|
+
*
|
|
3248
|
+
* @example
|
|
3249
|
+
* ```ts
|
|
3250
|
+
* // a→b, b→c, a→c
|
|
3251
|
+
* const reduced = getTransitiveReduction(graph);
|
|
3252
|
+
* // a→c removed; edges: a→b, b→c
|
|
3253
|
+
* ```
|
|
3254
|
+
*/
|
|
3255
|
+
function getTransitiveReduction(graph) {
|
|
3256
|
+
for (const edge of graph.edges) {
|
|
3257
|
+
const mode = getEdgeMode(graph, edge);
|
|
3258
|
+
if (mode !== "directed") throw new Error(`getTransitiveReduction: edge "${edge.id}" has effective mode "${mode}" — transitive reduction is only defined for directed acyclic graphs. Set edge.mode (or graph.mode) to 'directed'.`);
|
|
3259
|
+
}
|
|
3260
|
+
if (getEffectiveModeKind(graph) === "directed" && !isAcyclic(graph)) throw new Error("getTransitiveReduction: the graph contains a cycle — transitive reduction is only defined for directed acyclic graphs. Remove the cycle (see getCycles) first.");
|
|
3261
|
+
const successorsOf = /* @__PURE__ */ new Map();
|
|
3262
|
+
for (const node of graph.nodes) successorsOf.set(node.id, /* @__PURE__ */ new Set());
|
|
3263
|
+
for (const edge of graph.edges) successorsOf.get(edge.sourceId)?.add(edge.targetId);
|
|
3264
|
+
const reach = /* @__PURE__ */ new Map();
|
|
3265
|
+
function getReach(id) {
|
|
3266
|
+
let set = reach.get(id);
|
|
3267
|
+
if (set) return set;
|
|
3268
|
+
set = new Set([id]);
|
|
3269
|
+
reach.set(id, set);
|
|
3270
|
+
for (const succ of successorsOf.get(id) ?? []) for (const reached of getReach(succ)) set.add(reached);
|
|
3271
|
+
return set;
|
|
3272
|
+
}
|
|
3273
|
+
function isRedundant(u, v) {
|
|
3274
|
+
for (const w of successorsOf.get(u)) if (w !== v && getReach(w).has(v)) return true;
|
|
3275
|
+
return false;
|
|
3276
|
+
}
|
|
3277
|
+
const keptPairs = /* @__PURE__ */ new Set();
|
|
3278
|
+
const keptEdges = [];
|
|
3279
|
+
for (const edge of graph.edges) {
|
|
3280
|
+
const pair = `${edge.sourceId}${edge.targetId}`;
|
|
3281
|
+
if (keptPairs.has(pair)) continue;
|
|
3282
|
+
if (isRedundant(edge.sourceId, edge.targetId)) continue;
|
|
3283
|
+
keptPairs.add(pair);
|
|
3284
|
+
keptEdges.push(edge);
|
|
3285
|
+
}
|
|
3286
|
+
return createGraph({
|
|
3287
|
+
id: graph.id,
|
|
3288
|
+
mode: graph.mode,
|
|
3289
|
+
initialNodeId: graph.initialNodeId ?? void 0,
|
|
3290
|
+
data: graph.data ?? void 0,
|
|
3291
|
+
direction: graph.direction,
|
|
3292
|
+
style: graph.style,
|
|
3293
|
+
nodes: graph.nodes.map((node) => {
|
|
3294
|
+
const { type, parentId, initialNodeId, ...rest } = node;
|
|
3295
|
+
return {
|
|
3296
|
+
...rest,
|
|
3297
|
+
parentId: parentId ?? void 0,
|
|
3298
|
+
initialNodeId: initialNodeId ?? void 0
|
|
3299
|
+
};
|
|
3300
|
+
}),
|
|
3301
|
+
edges: keptEdges.map((edge) => {
|
|
3302
|
+
const { type, ...rest } = edge;
|
|
3303
|
+
return rest;
|
|
3304
|
+
})
|
|
3305
|
+
});
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3308
|
+
//#endregion
|
|
3309
|
+
export { getAStarPath as $, genPreorders as A, getTopologicalSort as B, getHITS as C, getPageRank as D, getOutDegreeCentrality as E, bfs as F, flatten as G, isAcyclic as H, dfs as I, getSubgraph as J, getFlattenedGraph as K, genBFS as L, getPostorders as M, getPreorder as N, getMinimumSpanningTree as O, getPreorders as P, genSimplePaths as Q, genDFS as R, getEigenvectorCentrality as S, getKatzCentrality as T, isConnected as U, hasPath as V, isTree as W, genCycles as X, reverseGraph as Y, genShortestPaths as Z, getCoreNumbers as _, getLouvainCommunities as a, getSimplePath as at, getClosenessCentrality as b, getBiconnectedComponents as c, joinPaths as ct, getGirvanNewmanCommunities as d, getAllPairsShortestPaths as et, getGreedyModularityCommunities as f, isBipartite as g, getMaximumBipartiteMatching as h, getMinCut as i, getShortestPaths as it, getPostorder as j, genPostorders as k, getBridges as l, mulberry32 as lt, getModularity as m, getDominatorTree as n, getJoinedPath as nt, isIsomorphic as o, getSimplePaths as ot, getLabelPropagationCommunities as p, getReversedGraph as q, getMaxFlow as r, getShortestPath as rt, getArticulationPoints as s, getStronglyConnectedComponents as st, getTransitiveReduction as t, getCycles as tt, genGirvanNewmanCommunities as u, getKCore as v, getInDegreeCentrality as w, getDegreeCentrality as x, getBetweennessCentrality as y, getConnectedComponents as z };
|