@statelyai/graph 1.0.0 → 2.0.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 +55 -26
- package/dist/{adjacency-list-VsUaH9SJ.mjs → adjacency-list-GeL1Cu-L.mjs} +3 -1
- package/dist/{algorithms-fTqmvhzP.d.mts → algorithms-CsGNehct.d.mts} +137 -2
- package/dist/{algorithms-Ba7o7niK.mjs → algorithms-DF1pSQGv.mjs} +1476 -343
- package/dist/algorithms.d.mts +2 -2
- package/dist/algorithms.mjs +2 -2
- package/dist/{converter-udLITX36.mjs → converter-DyCJJfTe.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 +1 -1
- package/dist/formats/cytoscape/index.mjs +3 -1
- package/dist/formats/d2/index.d.mts +1 -1
- package/dist/formats/d2/index.mjs +26 -12
- package/dist/formats/d3/index.d.mts +1 -1
- package/dist/formats/d3/index.mjs +3 -1
- 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 +21 -14
- package/dist/formats/gexf/index.d.mts +1 -1
- package/dist/formats/gexf/index.mjs +22 -15
- package/dist/formats/gml/index.d.mts +1 -1
- package/dist/formats/gml/index.mjs +21 -12
- package/dist/formats/graphml/index.d.mts +1 -1
- package/dist/formats/graphml/index.mjs +73 -22
- package/dist/formats/jgf/index.d.mts +1 -1
- package/dist/formats/jgf/index.mjs +5 -2
- package/dist/formats/mermaid/index.d.mts +1 -1
- package/dist/formats/mermaid/index.mjs +49 -12
- package/dist/formats/tgf/index.d.mts +1 -1
- package/dist/formats/tgf/index.mjs +1 -1
- package/dist/formats/xyflow/index.d.mts +1 -1
- package/dist/formats/xyflow/index.mjs +31 -4
- package/dist/{index-D9Kj6Fe3.d.mts → index-D51lJnt2.d.mts} +1 -1
- package/dist/{index-CHoriXZD.d.mts → index-DWmo1mIp.d.mts} +77 -18
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +143 -295
- package/dist/{queries-BlkA1HAN.d.mts → queries-BfXeTXRf.d.mts} +43 -12
- package/dist/queries-KirMDR7e.mjs +980 -0
- package/dist/queries.d.mts +1 -1
- package/dist/queries.mjs +1 -768
- package/dist/schemas.d.mts +1 -1
- package/dist/schemas.mjs +23 -84
- package/dist/{types-3-FS9NV2.d.mts → types-DNYdIU21.d.mts} +54 -5
- package/dist/validate-TtH-x3JV.mjs +190 -0
- package/package.json +13 -3
- package/dist/indexing-DR8M1vBy.mjs +0 -137
- /package/dist/{edge-list-DP4otyPU.mjs → edge-list-BcZ0h6zz.mjs} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { A as indexAddNode, M as indexUpdateEdgeEndpoints, N as invalidateIndex, O as getIndex, P as touchIndex, j as indexReparentNode, k as indexAddEdge, l as getInDegree, p as getOutDegree, r as getDegree } from "./queries-KirMDR7e.mjs";
|
|
2
2
|
import { t as getEdgeMode } from "./mode-D8OnHFBk.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/graph.ts
|
|
@@ -406,15 +406,46 @@ function deleteEdge(graph, id) {
|
|
|
406
406
|
graph.edges = graph.edges.filter((e) => e.id !== id);
|
|
407
407
|
invalidateIndex(graph);
|
|
408
408
|
}
|
|
409
|
+
/** Optional fields where `null` in an update unsets the field. */
|
|
410
|
+
const NODE_OPTIONAL_KEYS = [
|
|
411
|
+
"x",
|
|
412
|
+
"y",
|
|
413
|
+
"width",
|
|
414
|
+
"height",
|
|
415
|
+
"shape",
|
|
416
|
+
"color",
|
|
417
|
+
"style"
|
|
418
|
+
];
|
|
419
|
+
const EDGE_OPTIONAL_KEYS = [
|
|
420
|
+
"weight",
|
|
421
|
+
"mode",
|
|
422
|
+
"x",
|
|
423
|
+
"y",
|
|
424
|
+
"width",
|
|
425
|
+
"height",
|
|
426
|
+
"color",
|
|
427
|
+
"style"
|
|
428
|
+
];
|
|
429
|
+
/** Apply optional-field updates: `null` unsets, a value sets, `undefined` is ignored. */
|
|
430
|
+
function applyOptionalUpdates(target, update, keys) {
|
|
431
|
+
for (const key of keys) {
|
|
432
|
+
const value = update[key];
|
|
433
|
+
if (value === void 0) continue;
|
|
434
|
+
if (value === null) delete target[key];
|
|
435
|
+
else target[key] = value;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
409
438
|
/**
|
|
410
439
|
* **Mutable.** Update a node in place.
|
|
440
|
+
* Optional fields (`x`, `y`, `width`, `height`, `shape`, `color`, `style`,
|
|
441
|
+
* `ports`) accept `null` to unset; `undefined` leaves them unchanged.
|
|
411
442
|
* @returns The updated node.
|
|
412
443
|
*
|
|
413
444
|
* @example
|
|
414
445
|
* ```ts
|
|
415
446
|
* const graph = createGraph({ nodes: [{ id: 'a', label: 'old' }] });
|
|
416
|
-
* const updated = updateNode(graph, 'a', { label: 'new' });
|
|
417
|
-
* // updated.label === 'new'
|
|
447
|
+
* const updated = updateNode(graph, 'a', { label: 'new', x: 100 });
|
|
448
|
+
* // updated.label === 'new', updated.x === 100
|
|
418
449
|
* ```
|
|
419
450
|
*/
|
|
420
451
|
function updateNode(graph, id, update) {
|
|
@@ -423,24 +454,48 @@ function updateNode(graph, id, update) {
|
|
|
423
454
|
if (arrayIdx === void 0) throw new Error(`Node "${id}" does not exist`);
|
|
424
455
|
if (update.parentId !== void 0 && update.parentId !== null) {
|
|
425
456
|
if (!idx.nodeById.has(update.parentId)) throw new Error(`Parent node "${update.parentId}" does not exist`);
|
|
457
|
+
let ancestorId = update.parentId;
|
|
458
|
+
const seen = /* @__PURE__ */ new Set();
|
|
459
|
+
while (ancestorId !== null && !seen.has(ancestorId)) {
|
|
460
|
+
if (ancestorId === id) throw new Error(`Cannot set parentId of node "${id}" to "${update.parentId}": "${update.parentId}" is "${id}" or one of its descendants, which would create a hierarchy cycle. Reparent "${update.parentId}" elsewhere first.`);
|
|
461
|
+
seen.add(ancestorId);
|
|
462
|
+
const ai = idx.nodeById.get(ancestorId);
|
|
463
|
+
ancestorId = ai !== void 0 ? graph.nodes[ai].parentId ?? null : null;
|
|
464
|
+
}
|
|
426
465
|
}
|
|
427
|
-
if (update.ports
|
|
466
|
+
if (update.ports != null && update.ports.length > 0) validatePortNames(update.ports);
|
|
428
467
|
const node = graph.nodes[arrayIdx];
|
|
468
|
+
if (update.ports !== void 0) {
|
|
469
|
+
const newPortNames = new Set((update.ports ?? []).map((p) => p.name));
|
|
470
|
+
for (const eid of idx.outEdges.get(id) ?? []) {
|
|
471
|
+
const e = graph.edges[idx.edgeById.get(eid)];
|
|
472
|
+
if (e.sourcePort !== void 0 && !newPortNames.has(e.sourcePort)) throw new Error(`Cannot update ports of node "${id}": edge "${e.id}" references port "${e.sourcePort}" via sourcePort. Keep that port, or update/delete the edge first.`);
|
|
473
|
+
}
|
|
474
|
+
for (const eid of idx.inEdges.get(id) ?? []) {
|
|
475
|
+
const e = graph.edges[idx.edgeById.get(eid)];
|
|
476
|
+
if (e.targetPort !== void 0 && !newPortNames.has(e.targetPort)) throw new Error(`Cannot update ports of node "${id}": edge "${e.id}" references port "${e.targetPort}" via targetPort. Keep that port, or update/delete the edge first.`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
429
479
|
const oldParentId = node.parentId;
|
|
430
480
|
const updated = {
|
|
431
481
|
...node,
|
|
432
482
|
...update.parentId !== void 0 && { parentId: update.parentId ?? null },
|
|
433
483
|
...update.initialNodeId !== void 0 && { initialNodeId: update.initialNodeId ?? null },
|
|
434
484
|
...update.label !== void 0 && { label: update.label },
|
|
435
|
-
...update.data !== void 0 && { data: update.data }
|
|
436
|
-
...update.ports !== void 0 && { ports: update.ports.map(createGraphPort) }
|
|
485
|
+
...update.data !== void 0 && { data: update.data }
|
|
437
486
|
};
|
|
487
|
+
if (update.ports !== void 0) if (update.ports === null) delete updated.ports;
|
|
488
|
+
else updated.ports = update.ports.map(createGraphPort);
|
|
489
|
+
applyOptionalUpdates(updated, update, NODE_OPTIONAL_KEYS);
|
|
438
490
|
graph.nodes[arrayIdx] = updated;
|
|
439
491
|
if (update.parentId !== void 0 && updated.parentId !== oldParentId) indexReparentNode(idx, id, oldParentId, updated.parentId);
|
|
440
492
|
return updated;
|
|
441
493
|
}
|
|
442
494
|
/**
|
|
443
495
|
* **Mutable.** Update an edge in place.
|
|
496
|
+
* Optional fields (`weight`, `mode`, `sourcePort`, `targetPort`, `x`, `y`,
|
|
497
|
+
* `width`, `height`, `color`, `style`) accept `null` to unset; `undefined`
|
|
498
|
+
* leaves them unchanged.
|
|
444
499
|
* @returns The updated edge.
|
|
445
500
|
*
|
|
446
501
|
* @example
|
|
@@ -449,8 +504,8 @@ function updateNode(graph, id, update) {
|
|
|
449
504
|
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
450
505
|
* edges: [{ id: 'e1', sourceId: 'a', targetId: 'b', label: 'old' }],
|
|
451
506
|
* });
|
|
452
|
-
* const updated = updateEdge(graph, 'e1', { label: 'new' });
|
|
453
|
-
* // updated.label === 'new'
|
|
507
|
+
* const updated = updateEdge(graph, 'e1', { label: 'new', weight: 2 });
|
|
508
|
+
* // updated.label === 'new', updated.weight === 2
|
|
454
509
|
* ```
|
|
455
510
|
*/
|
|
456
511
|
function updateEdge(graph, id, update) {
|
|
@@ -464,22 +519,28 @@ function updateEdge(graph, id, update) {
|
|
|
464
519
|
const oldTargetId = edge.targetId;
|
|
465
520
|
const effectiveSourceId = update.sourceId ?? edge.sourceId;
|
|
466
521
|
const effectiveTargetId = update.targetId ?? edge.targetId;
|
|
467
|
-
|
|
468
|
-
|
|
522
|
+
const effectiveSourcePort = update.sourcePort !== void 0 ? update.sourcePort ?? void 0 : edge.sourcePort;
|
|
523
|
+
const effectiveTargetPort = update.targetPort !== void 0 ? update.targetPort ?? void 0 : edge.targetPort;
|
|
524
|
+
if (effectiveSourcePort !== void 0) {
|
|
525
|
+
if (!graph.nodes[idx.nodeById.get(effectiveSourceId)].ports?.some((p) => p.name === effectiveSourcePort)) throw new Error(update.sourcePort !== void 0 ? `Port "${effectiveSourcePort}" does not exist on source node "${effectiveSourceId}"` : `Cannot update edge "${id}": its sourcePort "${effectiveSourcePort}" does not exist on the new source node "${effectiveSourceId}". Include sourcePort in the update (a port on "${effectiveSourceId}", or null to clear it).`);
|
|
469
526
|
}
|
|
470
|
-
if (
|
|
471
|
-
if (!graph.nodes[idx.nodeById.get(effectiveTargetId)].ports?.some((p) => p.name ===
|
|
527
|
+
if (effectiveTargetPort !== void 0) {
|
|
528
|
+
if (!graph.nodes[idx.nodeById.get(effectiveTargetId)].ports?.some((p) => p.name === effectiveTargetPort)) throw new Error(update.targetPort !== void 0 ? `Port "${effectiveTargetPort}" does not exist on target node "${effectiveTargetId}"` : `Cannot update edge "${id}": its targetPort "${effectiveTargetPort}" does not exist on the new target node "${effectiveTargetId}". Include targetPort in the update (a port on "${effectiveTargetId}", or null to clear it).`);
|
|
472
529
|
}
|
|
473
530
|
const updated = {
|
|
474
531
|
...edge,
|
|
475
532
|
...update.sourceId !== void 0 && { sourceId: update.sourceId },
|
|
476
533
|
...update.targetId !== void 0 && { targetId: update.targetId },
|
|
477
534
|
...update.label !== void 0 && { label: update.label },
|
|
478
|
-
...update.data !== void 0 && { data: update.data }
|
|
479
|
-
...update.sourcePort !== void 0 && { sourcePort: update.sourcePort },
|
|
480
|
-
...update.targetPort !== void 0 && { targetPort: update.targetPort }
|
|
535
|
+
...update.data !== void 0 && { data: update.data }
|
|
481
536
|
};
|
|
537
|
+
if (update.sourcePort !== void 0) if (update.sourcePort === null) delete updated.sourcePort;
|
|
538
|
+
else updated.sourcePort = update.sourcePort;
|
|
539
|
+
if (update.targetPort !== void 0) if (update.targetPort === null) delete updated.targetPort;
|
|
540
|
+
else updated.targetPort = update.targetPort;
|
|
541
|
+
applyOptionalUpdates(updated, update, EDGE_OPTIONAL_KEYS);
|
|
482
542
|
graph.edges[arrayIdx] = updated;
|
|
543
|
+
if (update.mode !== void 0 || update.weight !== void 0) touchIndex(idx);
|
|
483
544
|
if (updated.sourceId !== oldSourceId || updated.targetId !== oldTargetId) indexUpdateEdgeEndpoints(idx, id, oldSourceId, oldTargetId, updated.sourceId, updated.targetId);
|
|
484
545
|
return updated;
|
|
485
546
|
}
|
|
@@ -666,6 +727,9 @@ var MinPriorityQueue = class {
|
|
|
666
727
|
this.items.push(item);
|
|
667
728
|
this.bubbleUp(this.items.length - 1);
|
|
668
729
|
}
|
|
730
|
+
peek() {
|
|
731
|
+
return this.items[0];
|
|
732
|
+
}
|
|
669
733
|
pop() {
|
|
670
734
|
if (this.items.length === 0) return void 0;
|
|
671
735
|
const first = this.items[0];
|
|
@@ -699,7 +763,24 @@ var MinPriorityQueue = class {
|
|
|
699
763
|
}
|
|
700
764
|
}
|
|
701
765
|
};
|
|
702
|
-
|
|
766
|
+
/**
|
|
767
|
+
* Classify a graph by the *effective* mode of its edges (per-edge `mode`
|
|
768
|
+
* overrides included): all-directed, all-non-directed, or genuinely mixed.
|
|
769
|
+
* Edge-less graphs fall back to `graph.mode`.
|
|
770
|
+
*/
|
|
771
|
+
function getEffectiveModeKind(graph) {
|
|
772
|
+
let sawDirected = false;
|
|
773
|
+
let sawNonDirected = false;
|
|
774
|
+
for (const edge of graph.edges) {
|
|
775
|
+
if (getEdgeMode(graph, edge) === "directed") sawDirected = true;
|
|
776
|
+
else sawNonDirected = true;
|
|
777
|
+
if (sawDirected && sawNonDirected) return "mixed";
|
|
778
|
+
}
|
|
779
|
+
if (sawDirected) return "directed";
|
|
780
|
+
if (sawNonDirected) return "non-directed";
|
|
781
|
+
return graph.mode === "directed" ? "directed" : "non-directed";
|
|
782
|
+
}
|
|
783
|
+
function getNeighborIds(graph, nodeId) {
|
|
703
784
|
const idx = getIndex(graph);
|
|
704
785
|
const ids = [];
|
|
705
786
|
for (const eid of idx.outEdges.get(nodeId) ?? []) {
|
|
@@ -779,67 +860,181 @@ function getNeighborEdgesAll(graph, nodeId) {
|
|
|
779
860
|
return result;
|
|
780
861
|
}
|
|
781
862
|
|
|
863
|
+
//#endregion
|
|
864
|
+
//#region src/algorithms/csr.ts
|
|
865
|
+
const csrCache = /* @__PURE__ */ new WeakMap();
|
|
866
|
+
/** Get or lazily (re)build the CSR snapshot for a graph. */
|
|
867
|
+
function getCSR(graph) {
|
|
868
|
+
const idx = getIndex(graph);
|
|
869
|
+
const cached = csrCache.get(idx);
|
|
870
|
+
if (cached && cached.version === idx.version && cached.mode === graph.mode) return cached.csr;
|
|
871
|
+
const csr = buildCSR(graph);
|
|
872
|
+
csrCache.set(idx, {
|
|
873
|
+
version: idx.version,
|
|
874
|
+
mode: graph.mode,
|
|
875
|
+
csr
|
|
876
|
+
});
|
|
877
|
+
return csr;
|
|
878
|
+
}
|
|
879
|
+
function buildCSR(graph) {
|
|
880
|
+
const n = graph.nodes.length;
|
|
881
|
+
const m = graph.edges.length;
|
|
882
|
+
const ids = new Array(n);
|
|
883
|
+
const indexOf = /* @__PURE__ */ new Map();
|
|
884
|
+
for (let i = 0; i < n; i++) {
|
|
885
|
+
ids[i] = graph.nodes[i].id;
|
|
886
|
+
indexOf.set(ids[i], i);
|
|
887
|
+
}
|
|
888
|
+
const srcPos = new Int32Array(m);
|
|
889
|
+
const tgtPos = new Int32Array(m);
|
|
890
|
+
const nonDirected = new Uint8Array(m);
|
|
891
|
+
const outCounts = new Int32Array(n);
|
|
892
|
+
const inCounts = new Int32Array(n);
|
|
893
|
+
let firstNegativeEdge = -1;
|
|
894
|
+
for (let e = 0; e < m; e++) {
|
|
895
|
+
const edge = graph.edges[e];
|
|
896
|
+
if (firstNegativeEdge === -1 && (edge.weight ?? 1) < 0) firstNegativeEdge = e;
|
|
897
|
+
const s = indexOf.get(edge.sourceId);
|
|
898
|
+
const t = indexOf.get(edge.targetId);
|
|
899
|
+
if (s === void 0 || t === void 0) {
|
|
900
|
+
srcPos[e] = -1;
|
|
901
|
+
tgtPos[e] = -1;
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
srcPos[e] = s;
|
|
905
|
+
tgtPos[e] = t;
|
|
906
|
+
const nd = getEdgeMode(graph, edge) !== "directed" ? 1 : 0;
|
|
907
|
+
nonDirected[e] = nd;
|
|
908
|
+
outCounts[s]++;
|
|
909
|
+
inCounts[t]++;
|
|
910
|
+
if (nd) {
|
|
911
|
+
outCounts[t]++;
|
|
912
|
+
inCounts[s]++;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
const outOffsets = new Int32Array(n + 1);
|
|
916
|
+
const inOffsets = new Int32Array(n + 1);
|
|
917
|
+
for (let i = 0; i < n; i++) {
|
|
918
|
+
outOffsets[i + 1] = outOffsets[i] + outCounts[i];
|
|
919
|
+
inOffsets[i + 1] = inOffsets[i] + inCounts[i];
|
|
920
|
+
}
|
|
921
|
+
const outTargets = new Int32Array(outOffsets[n]);
|
|
922
|
+
const outEdgeIndex = new Int32Array(outOffsets[n]);
|
|
923
|
+
const inOrigins = new Int32Array(inOffsets[n]);
|
|
924
|
+
const inEdgeIndex = new Int32Array(inOffsets[n]);
|
|
925
|
+
const outCursor = outOffsets.slice(0, n);
|
|
926
|
+
const inCursor = inOffsets.slice(0, n);
|
|
927
|
+
for (let e = 0; e < m; e++) {
|
|
928
|
+
const s = srcPos[e];
|
|
929
|
+
const t = tgtPos[e];
|
|
930
|
+
if (s < 0) continue;
|
|
931
|
+
outTargets[outCursor[s]] = t;
|
|
932
|
+
outEdgeIndex[outCursor[s]++] = e;
|
|
933
|
+
inOrigins[inCursor[t]] = s;
|
|
934
|
+
inEdgeIndex[inCursor[t]++] = e;
|
|
935
|
+
if (nonDirected[e]) {
|
|
936
|
+
outTargets[outCursor[t]] = s;
|
|
937
|
+
outEdgeIndex[outCursor[t]++] = e;
|
|
938
|
+
inOrigins[inCursor[s]] = t;
|
|
939
|
+
inEdgeIndex[inCursor[s]++] = e;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
return {
|
|
943
|
+
ids,
|
|
944
|
+
indexOf,
|
|
945
|
+
outOffsets,
|
|
946
|
+
outTargets,
|
|
947
|
+
outEdgeIndex,
|
|
948
|
+
inOffsets,
|
|
949
|
+
inOrigins,
|
|
950
|
+
inEdgeIndex,
|
|
951
|
+
firstNegativeEdge
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
|
|
782
955
|
//#endregion
|
|
783
956
|
//#region src/algorithms/paths.ts
|
|
784
|
-
function computeShortestDistances(graph, sourceId, getWeight, algorithm) {
|
|
957
|
+
function computeShortestDistances(graph, sourceId, getWeight, algorithm, stopAtId) {
|
|
785
958
|
if (algorithm === "bellman-ford") return bellmanFord(graph, sourceId, getWeight);
|
|
786
959
|
const dist = /* @__PURE__ */ new Map();
|
|
787
960
|
const prev = /* @__PURE__ */ new Map();
|
|
788
961
|
dist.set(sourceId, 0);
|
|
789
962
|
prev.set(sourceId, []);
|
|
963
|
+
const csr = getCSR(graph);
|
|
964
|
+
const source = csr.indexOf.get(sourceId);
|
|
965
|
+
if (source === void 0) return {
|
|
966
|
+
dist,
|
|
967
|
+
prev
|
|
968
|
+
};
|
|
969
|
+
const n = csr.ids.length;
|
|
970
|
+
const distArr = new Float64Array(n).fill(Infinity);
|
|
971
|
+
const prevArr = new Array(n);
|
|
972
|
+
distArr[source] = 0;
|
|
973
|
+
prevArr[source] = [];
|
|
974
|
+
const stopAt = stopAtId !== void 0 ? csr.indexOf.get(stopAtId) : void 0;
|
|
975
|
+
let stopDistance = Infinity;
|
|
976
|
+
if (stopAt !== void 0) assertNoNegativeWeights(graph, csr, getWeight, "Dijkstra", "Use { algorithm: 'bellman-ford' } instead.");
|
|
790
977
|
if (!getWeight && !graph.edges.some((edge) => edge.weight !== void 0)) {
|
|
791
|
-
const queue =
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
edge
|
|
808
|
-
});
|
|
978
|
+
const queue = new Int32Array(n);
|
|
979
|
+
queue[0] = source;
|
|
980
|
+
let head = 0;
|
|
981
|
+
let tail = 1;
|
|
982
|
+
while (head < tail) {
|
|
983
|
+
const u = queue[head++];
|
|
984
|
+
if (distArr[u] > stopDistance) break;
|
|
985
|
+
if (u === stopAt) stopDistance = distArr[u];
|
|
986
|
+
const nextDistance = distArr[u] + 1;
|
|
987
|
+
for (let a = csr.outOffsets[u]; a < csr.outOffsets[u + 1]; a++) {
|
|
988
|
+
const v = csr.outTargets[a];
|
|
989
|
+
if (distArr[v] === Infinity) {
|
|
990
|
+
distArr[v] = nextDistance;
|
|
991
|
+
prevArr[v] = [u, csr.outEdgeIndex[a]];
|
|
992
|
+
queue[tail++] = v;
|
|
993
|
+
} else if (distArr[v] === nextDistance) prevArr[v].push(u, csr.outEdgeIndex[a]);
|
|
809
994
|
}
|
|
810
995
|
}
|
|
811
996
|
} else {
|
|
812
997
|
const effectiveWeight = getWeight ?? ((edge) => edge.weight ?? 1);
|
|
813
|
-
const visited =
|
|
998
|
+
const visited = new Uint8Array(n);
|
|
814
999
|
const pq = new MinPriorityQueue((a, b) => a.dist - b.dist);
|
|
815
1000
|
pq.push({
|
|
816
|
-
|
|
1001
|
+
pos: source,
|
|
817
1002
|
dist: 0
|
|
818
1003
|
});
|
|
819
1004
|
while (pq.size > 0) {
|
|
820
|
-
const {
|
|
821
|
-
if (visited
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
1005
|
+
const { pos: u, dist: distance } = pq.pop();
|
|
1006
|
+
if (visited[u] || distance !== distArr[u]) continue;
|
|
1007
|
+
if (distance > stopDistance) break;
|
|
1008
|
+
if (u === stopAt) stopDistance = distance;
|
|
1009
|
+
visited[u] = 1;
|
|
1010
|
+
for (let a = csr.outOffsets[u]; a < csr.outOffsets[u + 1]; a++) {
|
|
1011
|
+
const edge = graph.edges[csr.outEdgeIndex[a]];
|
|
1012
|
+
const weight = effectiveWeight(edge);
|
|
1013
|
+
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.`);
|
|
1014
|
+
const v = csr.outTargets[a];
|
|
1015
|
+
const nextDistance = distance + weight;
|
|
1016
|
+
if (nextDistance < distArr[v]) {
|
|
1017
|
+
distArr[v] = nextDistance;
|
|
1018
|
+
prevArr[v] = [u, csr.outEdgeIndex[a]];
|
|
832
1019
|
pq.push({
|
|
833
|
-
|
|
1020
|
+
pos: v,
|
|
834
1021
|
dist: nextDistance
|
|
835
1022
|
});
|
|
836
|
-
} else if (
|
|
837
|
-
from: id,
|
|
838
|
-
edge
|
|
839
|
-
});
|
|
1023
|
+
} else if (nextDistance === distArr[v] && distArr[v] !== Infinity) prevArr[v].push(u, csr.outEdgeIndex[a]);
|
|
840
1024
|
}
|
|
841
1025
|
}
|
|
842
1026
|
}
|
|
1027
|
+
for (let i = 0; i < n; i++) {
|
|
1028
|
+
if (distArr[i] === Infinity || distArr[i] > stopDistance) continue;
|
|
1029
|
+
dist.set(csr.ids[i], distArr[i]);
|
|
1030
|
+
const pairs = prevArr[i];
|
|
1031
|
+
const predecessors = [];
|
|
1032
|
+
for (let k = 0; k < pairs.length; k += 2) predecessors.push({
|
|
1033
|
+
from: csr.ids[pairs[k]],
|
|
1034
|
+
edge: graph.edges[pairs[k + 1]]
|
|
1035
|
+
});
|
|
1036
|
+
prev.set(csr.ids[i], predecessors);
|
|
1037
|
+
}
|
|
843
1038
|
return {
|
|
844
1039
|
dist,
|
|
845
1040
|
prev
|
|
@@ -905,7 +1100,7 @@ function bellmanFord(graph, sourceId, getWeight) {
|
|
|
905
1100
|
prev
|
|
906
1101
|
};
|
|
907
1102
|
}
|
|
908
|
-
function* reconstructPaths(graph, prev, sourceNode, targetId) {
|
|
1103
|
+
function* reconstructPaths(graph, prev, sourceNode, targetId, onPath = /* @__PURE__ */ new Set()) {
|
|
909
1104
|
if (targetId === sourceNode.id) {
|
|
910
1105
|
yield {
|
|
911
1106
|
source: sourceNode,
|
|
@@ -917,18 +1112,23 @@ function* reconstructPaths(graph, prev, sourceNode, targetId) {
|
|
|
917
1112
|
if (!predecessors || predecessors.length === 0) return;
|
|
918
1113
|
const targetNi = getIndex(graph).nodeById.get(targetId);
|
|
919
1114
|
const targetNode = targetNi !== void 0 ? graph.nodes[targetNi] : graph.nodes.find((node) => node.id === targetId);
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
1115
|
+
onPath.add(targetId);
|
|
1116
|
+
for (const { from, edge } of predecessors) {
|
|
1117
|
+
if (onPath.has(from)) continue;
|
|
1118
|
+
for (const prefix of reconstructPaths(graph, prev, sourceNode, from, onPath)) yield {
|
|
1119
|
+
source: sourceNode,
|
|
1120
|
+
steps: [...prefix.steps, {
|
|
1121
|
+
edge,
|
|
1122
|
+
node: targetNode
|
|
1123
|
+
}]
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
onPath.delete(targetId);
|
|
927
1127
|
}
|
|
928
1128
|
function* genShortestPaths(graph, opts) {
|
|
929
1129
|
const idx = getIndex(graph);
|
|
930
1130
|
const sourceId = resolveFrom(graph, opts);
|
|
931
|
-
const { dist, prev } = computeShortestDistances(graph, sourceId, opts?.getWeight, opts?.algorithm);
|
|
1131
|
+
const { dist, prev } = computeShortestDistances(graph, sourceId, opts?.getWeight, opts?.algorithm, opts?.to);
|
|
932
1132
|
const targets = opts?.to ? [opts.to].filter((id) => dist.has(id)) : [...dist.keys()].filter((id) => id !== sourceId);
|
|
933
1133
|
const sourceNi = idx.nodeById.get(sourceId);
|
|
934
1134
|
const sourceNode = sourceNi !== void 0 ? graph.nodes[sourceNi] : graph.nodes.find((node) => node.id === sourceId);
|
|
@@ -938,8 +1138,161 @@ function getShortestPaths(graph, opts) {
|
|
|
938
1138
|
return [...genShortestPaths(graph, opts)];
|
|
939
1139
|
}
|
|
940
1140
|
function getShortestPath(graph, opts) {
|
|
1141
|
+
if (opts.algorithm !== "bellman-ford") return bidirectionalShortestPath(graph, resolveFrom(graph, opts), opts.to, opts.getWeight);
|
|
941
1142
|
for (const path of genShortestPaths(graph, opts)) return path;
|
|
942
1143
|
}
|
|
1144
|
+
/**
|
|
1145
|
+
* Sublinear searches (early-exit, bidirectional) may legitimately terminate
|
|
1146
|
+
* without ever scanning a negative edge, so the throw-on-negative contract
|
|
1147
|
+
* must be enforced up front: O(1) via the CSR's cached flag for the default
|
|
1148
|
+
* weight, or one O(edges) sweep for a custom `getWeight`.
|
|
1149
|
+
*/
|
|
1150
|
+
function assertNoNegativeWeights(graph, csr, getWeight, algorithmName, remedy) {
|
|
1151
|
+
let offending;
|
|
1152
|
+
let weight = 0;
|
|
1153
|
+
if (getWeight === void 0) {
|
|
1154
|
+
if (csr.firstNegativeEdge !== -1) {
|
|
1155
|
+
offending = graph.edges[csr.firstNegativeEdge];
|
|
1156
|
+
weight = offending.weight ?? 1;
|
|
1157
|
+
}
|
|
1158
|
+
} else for (const edge of graph.edges) {
|
|
1159
|
+
const w = getWeight(edge);
|
|
1160
|
+
if (w < 0) {
|
|
1161
|
+
offending = edge;
|
|
1162
|
+
weight = w;
|
|
1163
|
+
break;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
if (offending) throw new Error(`Negative edge weight ${weight} on edge "${offending.sourceId}->${offending.targetId}" (id "${offending.id}"): ${algorithmName} requires non-negative weights. ${remedy}`);
|
|
1167
|
+
}
|
|
1168
|
+
/**
|
|
1169
|
+
* Bidirectional Dijkstra for a single source→target query. Forward search
|
|
1170
|
+
* runs on the traversable arcs, backward search on the reverse arcs; `mu`
|
|
1171
|
+
* tracks the best meeting cost and the search stops when the two frontiers
|
|
1172
|
+
* prove no better meeting exists (Pohl's `topF + topB >= mu` condition).
|
|
1173
|
+
* Returns one shortest path (ties broken arbitrarily, as before).
|
|
1174
|
+
*/
|
|
1175
|
+
function bidirectionalShortestPath(graph, sourceId, targetId, getWeight) {
|
|
1176
|
+
const csr = getCSR(graph);
|
|
1177
|
+
const source = csr.indexOf.get(sourceId);
|
|
1178
|
+
const target = csr.indexOf.get(targetId);
|
|
1179
|
+
if (source === void 0 || target === void 0) return void 0;
|
|
1180
|
+
const sourceNode = graph.nodes[source];
|
|
1181
|
+
if (source === target) return {
|
|
1182
|
+
source: sourceNode,
|
|
1183
|
+
steps: []
|
|
1184
|
+
};
|
|
1185
|
+
assertNoNegativeWeights(graph, csr, getWeight, "Dijkstra", "Use { algorithm: 'bellman-ford' } instead.");
|
|
1186
|
+
const effectiveWeight = getWeight ?? ((edge) => edge.weight ?? 1);
|
|
1187
|
+
const n = csr.ids.length;
|
|
1188
|
+
const distF = new Float64Array(n).fill(Infinity);
|
|
1189
|
+
const distB = new Float64Array(n).fill(Infinity);
|
|
1190
|
+
const predF = new Int32Array(n).fill(-1);
|
|
1191
|
+
const predFEdge = new Int32Array(n).fill(-1);
|
|
1192
|
+
const predB = new Int32Array(n).fill(-1);
|
|
1193
|
+
const predBEdge = new Int32Array(n).fill(-1);
|
|
1194
|
+
const settledF = new Uint8Array(n);
|
|
1195
|
+
const settledB = new Uint8Array(n);
|
|
1196
|
+
const compare = (a, b) => a.dist - b.dist;
|
|
1197
|
+
const pqF = new MinPriorityQueue(compare);
|
|
1198
|
+
const pqB = new MinPriorityQueue(compare);
|
|
1199
|
+
distF[source] = 0;
|
|
1200
|
+
distB[target] = 0;
|
|
1201
|
+
pqF.push({
|
|
1202
|
+
pos: source,
|
|
1203
|
+
dist: 0
|
|
1204
|
+
});
|
|
1205
|
+
pqB.push({
|
|
1206
|
+
pos: target,
|
|
1207
|
+
dist: 0
|
|
1208
|
+
});
|
|
1209
|
+
let mu = Infinity;
|
|
1210
|
+
let meet = -1;
|
|
1211
|
+
/** Discard stale/settled heap entries; return the next valid key. */
|
|
1212
|
+
const validTop = (pq, dist, settled) => {
|
|
1213
|
+
for (;;) {
|
|
1214
|
+
const top = pq.peek();
|
|
1215
|
+
if (top === void 0) return void 0;
|
|
1216
|
+
if (settled[top.pos] || top.dist !== dist[top.pos]) {
|
|
1217
|
+
pq.pop();
|
|
1218
|
+
continue;
|
|
1219
|
+
}
|
|
1220
|
+
return top.dist;
|
|
1221
|
+
}
|
|
1222
|
+
};
|
|
1223
|
+
const scanForward = () => {
|
|
1224
|
+
const { pos: u, dist: d } = pqF.pop();
|
|
1225
|
+
settledF[u] = 1;
|
|
1226
|
+
for (let a = csr.outOffsets[u]; a < csr.outOffsets[u + 1]; a++) {
|
|
1227
|
+
const edge = graph.edges[csr.outEdgeIndex[a]];
|
|
1228
|
+
const weight = effectiveWeight(edge);
|
|
1229
|
+
const v = csr.outTargets[a];
|
|
1230
|
+
const next = d + weight;
|
|
1231
|
+
if (next < distF[v]) {
|
|
1232
|
+
distF[v] = next;
|
|
1233
|
+
predF[v] = u;
|
|
1234
|
+
predFEdge[v] = csr.outEdgeIndex[a];
|
|
1235
|
+
pqF.push({
|
|
1236
|
+
pos: v,
|
|
1237
|
+
dist: next
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
if (distB[v] !== Infinity && next + distB[v] < mu) {
|
|
1241
|
+
mu = next + distB[v];
|
|
1242
|
+
meet = v;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
};
|
|
1246
|
+
const scanBackward = () => {
|
|
1247
|
+
const { pos: u, dist: d } = pqB.pop();
|
|
1248
|
+
settledB[u] = 1;
|
|
1249
|
+
for (let a = csr.inOffsets[u]; a < csr.inOffsets[u + 1]; a++) {
|
|
1250
|
+
const edge = graph.edges[csr.inEdgeIndex[a]];
|
|
1251
|
+
const weight = effectiveWeight(edge);
|
|
1252
|
+
const v = csr.inOrigins[a];
|
|
1253
|
+
const next = d + weight;
|
|
1254
|
+
if (next < distB[v]) {
|
|
1255
|
+
distB[v] = next;
|
|
1256
|
+
predB[v] = u;
|
|
1257
|
+
predBEdge[v] = csr.inEdgeIndex[a];
|
|
1258
|
+
pqB.push({
|
|
1259
|
+
pos: v,
|
|
1260
|
+
dist: next
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
if (distF[v] !== Infinity && next + distF[v] < mu) {
|
|
1264
|
+
mu = next + distF[v];
|
|
1265
|
+
meet = v;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
};
|
|
1269
|
+
for (;;) {
|
|
1270
|
+
const topF = validTop(pqF, distF, settledF);
|
|
1271
|
+
const topB = validTop(pqB, distB, settledB);
|
|
1272
|
+
if (topF === void 0 || topB === void 0) break;
|
|
1273
|
+
if (topF + topB >= mu) break;
|
|
1274
|
+
if (topF <= topB) scanForward();
|
|
1275
|
+
else scanBackward();
|
|
1276
|
+
}
|
|
1277
|
+
if (meet === -1) return void 0;
|
|
1278
|
+
const steps = [];
|
|
1279
|
+
for (let v = meet; v !== source; v = predF[v]) steps.unshift({
|
|
1280
|
+
edge: graph.edges[predFEdge[v]],
|
|
1281
|
+
node: graph.nodes[v]
|
|
1282
|
+
});
|
|
1283
|
+
for (let v = meet; v !== target;) {
|
|
1284
|
+
const nextNode = predB[v];
|
|
1285
|
+
steps.push({
|
|
1286
|
+
edge: graph.edges[predBEdge[v]],
|
|
1287
|
+
node: graph.nodes[nextNode]
|
|
1288
|
+
});
|
|
1289
|
+
v = nextNode;
|
|
1290
|
+
}
|
|
1291
|
+
return {
|
|
1292
|
+
source: sourceNode,
|
|
1293
|
+
steps
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
943
1296
|
function getSimplePaths(graph, opts) {
|
|
944
1297
|
return [...genSimplePaths(graph, opts)];
|
|
945
1298
|
}
|
|
@@ -997,15 +1350,10 @@ function getStronglyConnectedComponents(graph) {
|
|
|
997
1350
|
indexCounter++;
|
|
998
1351
|
stack.push(id);
|
|
999
1352
|
onStack.add(id);
|
|
1000
|
-
for (const
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
if (!nodeIndex.has(neighborId)) {
|
|
1005
|
-
strongconnect(neighborId);
|
|
1006
|
-
lowlink.set(id, Math.min(lowlink.get(id), lowlink.get(neighborId)));
|
|
1007
|
-
} else if (onStack.has(neighborId)) lowlink.set(id, Math.min(lowlink.get(id), nodeIndex.get(neighborId)));
|
|
1008
|
-
}
|
|
1353
|
+
for (const neighborId of getNeighborIds(graph, id)) if (!nodeIndex.has(neighborId)) {
|
|
1354
|
+
strongconnect(neighborId);
|
|
1355
|
+
lowlink.set(id, Math.min(lowlink.get(id), lowlink.get(neighborId)));
|
|
1356
|
+
} else if (onStack.has(neighborId)) lowlink.set(id, Math.min(lowlink.get(id), nodeIndex.get(neighborId)));
|
|
1009
1357
|
if (lowlink.get(id) === nodeIndex.get(id)) {
|
|
1010
1358
|
const component = [];
|
|
1011
1359
|
let neighborId;
|
|
@@ -1025,7 +1373,9 @@ function getCycles(graph) {
|
|
|
1025
1373
|
return [...genCycles(graph)];
|
|
1026
1374
|
}
|
|
1027
1375
|
function* genCycles(graph) {
|
|
1028
|
-
|
|
1376
|
+
const kind = getEffectiveModeKind(graph);
|
|
1377
|
+
if (kind === "mixed") yield* genCyclesMixed(graph);
|
|
1378
|
+
else if (kind === "non-directed") yield* genCyclesUndirected(graph);
|
|
1029
1379
|
else yield* genCyclesDirected(graph);
|
|
1030
1380
|
}
|
|
1031
1381
|
function* genCyclesDirected(graph) {
|
|
@@ -1081,17 +1431,14 @@ function* genCyclesUndirected(graph) {
|
|
|
1081
1431
|
const startNi = idx.nodeById.get(startId);
|
|
1082
1432
|
const startNode = graph.nodes[startNi];
|
|
1083
1433
|
const found = [];
|
|
1084
|
-
function dfsFind(currentId,
|
|
1434
|
+
function dfsFind(currentId, arrivalEdgeId) {
|
|
1085
1435
|
visited.add(currentId);
|
|
1086
1436
|
for (const { neighborId, edge } of getNeighborEdgesAll(graph, currentId)) {
|
|
1087
|
-
if (
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
const innerIds = steps.map((step) => step.node.id).sort().join(",");
|
|
1093
|
-
if (!seen.has(innerIds)) {
|
|
1094
|
-
seen.add(innerIds);
|
|
1437
|
+
if (edge.id === arrivalEdgeId) continue;
|
|
1438
|
+
if (neighborId === startId && (steps.length >= 1 || edge.sourceId === edge.targetId)) {
|
|
1439
|
+
const cycleEdgeIds = [...steps.map((step) => step.edge.id), edge.id].sort().join(",");
|
|
1440
|
+
if (!seen.has(cycleEdgeIds)) {
|
|
1441
|
+
seen.add(cycleEdgeIds);
|
|
1095
1442
|
found.push({
|
|
1096
1443
|
source: startNode,
|
|
1097
1444
|
steps: [...steps, {
|
|
@@ -1106,7 +1453,7 @@ function* genCyclesUndirected(graph) {
|
|
|
1106
1453
|
edge,
|
|
1107
1454
|
node: graph.nodes[ni]
|
|
1108
1455
|
});
|
|
1109
|
-
dfsFind(neighborId,
|
|
1456
|
+
dfsFind(neighborId, edge.id);
|
|
1110
1457
|
steps.pop();
|
|
1111
1458
|
}
|
|
1112
1459
|
}
|
|
@@ -1116,6 +1463,59 @@ function* genCyclesUndirected(graph) {
|
|
|
1116
1463
|
yield* found;
|
|
1117
1464
|
}
|
|
1118
1465
|
}
|
|
1466
|
+
/**
|
|
1467
|
+
* Exact simple-cycle enumeration for graphs mixing directed and non-directed
|
|
1468
|
+
* edges. Traverses directed edges source→target only and non-directed edges
|
|
1469
|
+
* both ways; a cycle may use each edge at most once, visits distinct nodes,
|
|
1470
|
+
* and is identified by its set of traversed edge ids.
|
|
1471
|
+
*/
|
|
1472
|
+
function* genCyclesMixed(graph) {
|
|
1473
|
+
const idx = getIndex(graph);
|
|
1474
|
+
const sortedIds = graph.nodes.map((node) => node.id).sort();
|
|
1475
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1476
|
+
for (let startIndex = 0; startIndex < sortedIds.length; startIndex++) {
|
|
1477
|
+
const startId = sortedIds[startIndex];
|
|
1478
|
+
const allowed = new Set(sortedIds.slice(startIndex));
|
|
1479
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1480
|
+
const steps = [];
|
|
1481
|
+
const pathEdgeIds = /* @__PURE__ */ new Set();
|
|
1482
|
+
const startNi = idx.nodeById.get(startId);
|
|
1483
|
+
const startNode = graph.nodes[startNi];
|
|
1484
|
+
const found = [];
|
|
1485
|
+
function dfsFind(currentId) {
|
|
1486
|
+
visited.add(currentId);
|
|
1487
|
+
for (const { neighborId, edge } of getNeighborEdges(graph, currentId)) {
|
|
1488
|
+
if (pathEdgeIds.has(edge.id)) continue;
|
|
1489
|
+
if (neighborId === startId && (steps.length >= 1 || edge.sourceId === edge.targetId)) {
|
|
1490
|
+
const cycleEdgeIds = [...steps.map((step) => step.edge.id), edge.id].sort().join(",");
|
|
1491
|
+
if (!seen.has(cycleEdgeIds)) {
|
|
1492
|
+
seen.add(cycleEdgeIds);
|
|
1493
|
+
found.push({
|
|
1494
|
+
source: startNode,
|
|
1495
|
+
steps: [...steps, {
|
|
1496
|
+
edge,
|
|
1497
|
+
node: startNode
|
|
1498
|
+
}]
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
} else if (allowed.has(neighborId) && !visited.has(neighborId)) {
|
|
1502
|
+
const ni = idx.nodeById.get(neighborId);
|
|
1503
|
+
steps.push({
|
|
1504
|
+
edge,
|
|
1505
|
+
node: graph.nodes[ni]
|
|
1506
|
+
});
|
|
1507
|
+
pathEdgeIds.add(edge.id);
|
|
1508
|
+
dfsFind(neighborId);
|
|
1509
|
+
pathEdgeIds.delete(edge.id);
|
|
1510
|
+
steps.pop();
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
visited.delete(currentId);
|
|
1514
|
+
}
|
|
1515
|
+
dfsFind(startId);
|
|
1516
|
+
yield* found;
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1119
1519
|
function getAllPairsShortestPaths(graph, opts) {
|
|
1120
1520
|
const algorithm = opts?.algorithm ?? "dijkstra";
|
|
1121
1521
|
if (algorithm === "floyd-warshall") return floydWarshallAllPaths(graph, opts?.getWeight);
|
|
@@ -1187,6 +1587,7 @@ function floydWarshallAllPaths(graph, getWeight) {
|
|
|
1187
1587
|
for (const entry of prev[k][j]) if (!prev[i][j].some((existing) => existing.edge.id === entry.edge.id)) prev[i][j].push({ ...entry });
|
|
1188
1588
|
}
|
|
1189
1589
|
}
|
|
1590
|
+
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.`);
|
|
1190
1591
|
const results = [];
|
|
1191
1592
|
for (let i = 0; i < nodeCount; i++) {
|
|
1192
1593
|
const sourceNi = idx.nodeById.get(nodeIds[i]);
|
|
@@ -1233,48 +1634,53 @@ function getAStarPath(graph, opts) {
|
|
|
1233
1634
|
source: graph.nodes[sourceNi],
|
|
1234
1635
|
steps: []
|
|
1235
1636
|
};
|
|
1236
|
-
const
|
|
1237
|
-
const
|
|
1238
|
-
const
|
|
1637
|
+
const csr = getCSR(graph);
|
|
1638
|
+
const n = csr.ids.length;
|
|
1639
|
+
const source = csr.indexOf.get(sourceId);
|
|
1640
|
+
const target = csr.indexOf.get(targetId);
|
|
1641
|
+
const gScore = new Float64Array(n).fill(Infinity);
|
|
1642
|
+
const cameFromPos = new Int32Array(n).fill(-1);
|
|
1643
|
+
const cameFromEdge = new Int32Array(n).fill(-1);
|
|
1644
|
+
const closed = new Uint8Array(n);
|
|
1239
1645
|
const openSet = new MinPriorityQueue((a, b) => a.f - b.f);
|
|
1240
|
-
|
|
1646
|
+
assertNoNegativeWeights(graph, csr, opts.getWeight, "A*", "Use getShortestPath with { algorithm: 'bellman-ford' } instead.");
|
|
1647
|
+
gScore[source] = 0;
|
|
1241
1648
|
openSet.push({
|
|
1242
|
-
|
|
1649
|
+
pos: source,
|
|
1243
1650
|
f: heuristic(sourceId)
|
|
1244
1651
|
});
|
|
1245
1652
|
while (openSet.size > 0) {
|
|
1246
|
-
const {
|
|
1247
|
-
if (
|
|
1248
|
-
if (
|
|
1653
|
+
const { pos: current } = openSet.pop();
|
|
1654
|
+
if (closed[current]) continue;
|
|
1655
|
+
if (current === target) {
|
|
1249
1656
|
const steps = [];
|
|
1250
|
-
let
|
|
1251
|
-
while (
|
|
1252
|
-
const previous = cameFrom.get(current);
|
|
1253
|
-
const ni = idx.nodeById.get(current);
|
|
1657
|
+
let cursor = target;
|
|
1658
|
+
while (cursor !== source) {
|
|
1254
1659
|
steps.unshift({
|
|
1255
|
-
edge:
|
|
1256
|
-
node: graph.nodes[
|
|
1660
|
+
edge: graph.edges[cameFromEdge[cursor]],
|
|
1661
|
+
node: graph.nodes[cursor]
|
|
1257
1662
|
});
|
|
1258
|
-
|
|
1663
|
+
cursor = cameFromPos[cursor];
|
|
1259
1664
|
}
|
|
1260
1665
|
return {
|
|
1261
1666
|
source: graph.nodes[sourceNi],
|
|
1262
1667
|
steps
|
|
1263
1668
|
};
|
|
1264
1669
|
}
|
|
1265
|
-
|
|
1266
|
-
for (
|
|
1267
|
-
|
|
1268
|
-
const
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1670
|
+
closed[current] = 1;
|
|
1671
|
+
for (let a = csr.outOffsets[current]; a < csr.outOffsets[current + 1]; a++) {
|
|
1672
|
+
const edge = graph.edges[csr.outEdgeIndex[a]];
|
|
1673
|
+
const weight = getWeight(edge);
|
|
1674
|
+
const neighbor = csr.outTargets[a];
|
|
1675
|
+
if (closed[neighbor]) continue;
|
|
1676
|
+
const tentativeScore = gScore[current] + weight;
|
|
1677
|
+
if (tentativeScore < gScore[neighbor]) {
|
|
1678
|
+
cameFromPos[neighbor] = current;
|
|
1679
|
+
cameFromEdge[neighbor] = csr.outEdgeIndex[a];
|
|
1680
|
+
gScore[neighbor] = tentativeScore;
|
|
1275
1681
|
openSet.push({
|
|
1276
|
-
|
|
1277
|
-
f: tentativeScore + heuristic(
|
|
1682
|
+
pos: neighbor,
|
|
1683
|
+
f: tentativeScore + heuristic(csr.ids[neighbor])
|
|
1278
1684
|
});
|
|
1279
1685
|
}
|
|
1280
1686
|
}
|
|
@@ -1289,40 +1695,289 @@ function joinPaths(headPath, tailPath) {
|
|
|
1289
1695
|
};
|
|
1290
1696
|
}
|
|
1291
1697
|
|
|
1698
|
+
//#endregion
|
|
1699
|
+
//#region src/config.ts
|
|
1700
|
+
/**
|
|
1701
|
+
* Convert a resolved {@link GraphNode} back into a {@link NodeConfig}.
|
|
1702
|
+
*
|
|
1703
|
+
* Faithful and complete: round-tripping through `createGraphNode` yields a
|
|
1704
|
+
* deep-equal node. Optional fields are only included when present; ports are
|
|
1705
|
+
* deep-copied so the config does not share port objects with the source node.
|
|
1706
|
+
*/
|
|
1707
|
+
function toNodeConfig(node) {
|
|
1708
|
+
const config = { id: node.id };
|
|
1709
|
+
if (node.parentId != null) config.parentId = node.parentId;
|
|
1710
|
+
if (node.initialNodeId != null) config.initialNodeId = node.initialNodeId;
|
|
1711
|
+
if (node.label != null) config.label = node.label;
|
|
1712
|
+
if (node.data != null) config.data = node.data;
|
|
1713
|
+
if (node.ports !== void 0) config.ports = node.ports.map((p) => ({ ...p }));
|
|
1714
|
+
if (node.x !== void 0) config.x = node.x;
|
|
1715
|
+
if (node.y !== void 0) config.y = node.y;
|
|
1716
|
+
if (node.width !== void 0) config.width = node.width;
|
|
1717
|
+
if (node.height !== void 0) config.height = node.height;
|
|
1718
|
+
if (node.shape !== void 0) config.shape = node.shape;
|
|
1719
|
+
if (node.color !== void 0) config.color = node.color;
|
|
1720
|
+
if (node.style !== void 0) config.style = node.style;
|
|
1721
|
+
return config;
|
|
1722
|
+
}
|
|
1723
|
+
/**
|
|
1724
|
+
* Convert a resolved {@link GraphEdge} back into an {@link EdgeConfig}.
|
|
1725
|
+
*
|
|
1726
|
+
* Faithful and complete: round-tripping through `createGraphEdge` yields a
|
|
1727
|
+
* deep-equal edge. Optional fields are only included when present.
|
|
1728
|
+
*/
|
|
1729
|
+
function toEdgeConfig(edge) {
|
|
1730
|
+
const config = {
|
|
1731
|
+
id: edge.id,
|
|
1732
|
+
sourceId: edge.sourceId,
|
|
1733
|
+
targetId: edge.targetId
|
|
1734
|
+
};
|
|
1735
|
+
if (edge.label != null) config.label = edge.label;
|
|
1736
|
+
if (edge.data != null) config.data = edge.data;
|
|
1737
|
+
if (edge.weight !== void 0) config.weight = edge.weight;
|
|
1738
|
+
if (edge.mode !== void 0) config.mode = edge.mode;
|
|
1739
|
+
if (edge.sourcePort !== void 0) config.sourcePort = edge.sourcePort;
|
|
1740
|
+
if (edge.targetPort !== void 0) config.targetPort = edge.targetPort;
|
|
1741
|
+
if (edge.x !== void 0) config.x = edge.x;
|
|
1742
|
+
if (edge.y !== void 0) config.y = edge.y;
|
|
1743
|
+
if (edge.width !== void 0) config.width = edge.width;
|
|
1744
|
+
if (edge.height !== void 0) config.height = edge.height;
|
|
1745
|
+
if (edge.color !== void 0) config.color = edge.color;
|
|
1746
|
+
if (edge.style !== void 0) config.style = edge.style;
|
|
1747
|
+
return config;
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
//#endregion
|
|
1751
|
+
//#region src/transforms.ts
|
|
1752
|
+
/**
|
|
1753
|
+
* Flattens a hierarchical graph into a flat graph with only leaf nodes.
|
|
1754
|
+
*
|
|
1755
|
+
* - Edges targeting a compound node resolve to its initial child (recursively).
|
|
1756
|
+
* - Edges originating from a compound node expand to all leaf descendants.
|
|
1757
|
+
* - Only leaf nodes (nodes with no children) appear in the result.
|
|
1758
|
+
* - Duplicate edges (same source + target) are deduplicated.
|
|
1759
|
+
*
|
|
1760
|
+
* @example
|
|
1761
|
+
* ```ts
|
|
1762
|
+
* import { createGraph, flatten } from '@statelyai/graph';
|
|
1763
|
+
*
|
|
1764
|
+
* const graph = createGraph({
|
|
1765
|
+
* nodes: [
|
|
1766
|
+
* { id: 'parent', initialNodeId: 'child1' },
|
|
1767
|
+
* { id: 'child1', parentId: 'parent' },
|
|
1768
|
+
* { id: 'child2', parentId: 'parent' },
|
|
1769
|
+
* { id: 'other' },
|
|
1770
|
+
* ],
|
|
1771
|
+
* edges: [{ id: 'e1', sourceId: 'other', targetId: 'parent' }],
|
|
1772
|
+
* });
|
|
1773
|
+
*
|
|
1774
|
+
* const flat = flatten(graph);
|
|
1775
|
+
* // flat.nodes → [child1, child2, other] (leaf nodes only)
|
|
1776
|
+
* // flat.edges → edge from 'other' → 'child1' (resolved via initialNodeId)
|
|
1777
|
+
* ```
|
|
1778
|
+
*/
|
|
1779
|
+
function flatten(graph) {
|
|
1780
|
+
const idx = getIndex(graph);
|
|
1781
|
+
const leaves = /* @__PURE__ */ new Set();
|
|
1782
|
+
for (const node of graph.nodes) if ((idx.childNodes.get(node.id) ?? []).length === 0) leaves.add(node.id);
|
|
1783
|
+
function resolveInitial(nodeId, seen = /* @__PURE__ */ new Set()) {
|
|
1784
|
+
if (leaves.has(nodeId)) return nodeId;
|
|
1785
|
+
if (seen.has(nodeId)) return null;
|
|
1786
|
+
seen.add(nodeId);
|
|
1787
|
+
const ni = idx.nodeById.get(nodeId);
|
|
1788
|
+
if (ni === void 0) return null;
|
|
1789
|
+
const node = graph.nodes[ni];
|
|
1790
|
+
if (node.initialNodeId) return resolveInitial(node.initialNodeId, seen);
|
|
1791
|
+
const childIds = idx.childNodes.get(nodeId) ?? [];
|
|
1792
|
+
if (childIds.length > 0) return resolveInitial(childIds[0], seen);
|
|
1793
|
+
return nodeId;
|
|
1794
|
+
}
|
|
1795
|
+
function getLeafDescendants(nodeId) {
|
|
1796
|
+
if (leaves.has(nodeId)) return [nodeId];
|
|
1797
|
+
const result = [];
|
|
1798
|
+
const collect = (id) => {
|
|
1799
|
+
const childIds = idx.childNodes.get(id) ?? [];
|
|
1800
|
+
for (const childId of childIds) if (leaves.has(childId)) result.push(childId);
|
|
1801
|
+
else collect(childId);
|
|
1802
|
+
};
|
|
1803
|
+
collect(nodeId);
|
|
1804
|
+
return result;
|
|
1805
|
+
}
|
|
1806
|
+
const edgeSeen = /* @__PURE__ */ new Set();
|
|
1807
|
+
const flatEdges = [];
|
|
1808
|
+
for (const edge of graph.edges) {
|
|
1809
|
+
const sources = leaves.has(edge.sourceId) ? [edge.sourceId] : getLeafDescendants(edge.sourceId);
|
|
1810
|
+
const target = leaves.has(edge.targetId) ? edge.targetId : resolveInitial(edge.targetId);
|
|
1811
|
+
if (target === null) continue;
|
|
1812
|
+
for (const source of sources) {
|
|
1813
|
+
const isAuthoredLeafSelfLoop = edge.sourceId === edge.targetId && leaves.has(edge.sourceId);
|
|
1814
|
+
if (source === target && !isAuthoredLeafSelfLoop) continue;
|
|
1815
|
+
const key = `${source}->${target}`;
|
|
1816
|
+
if (edgeSeen.has(key)) continue;
|
|
1817
|
+
edgeSeen.add(key);
|
|
1818
|
+
flatEdges.push({
|
|
1819
|
+
type: "edge",
|
|
1820
|
+
id: `${edge.id}:${source}->${target}`,
|
|
1821
|
+
sourceId: source,
|
|
1822
|
+
targetId: target,
|
|
1823
|
+
label: edge.label,
|
|
1824
|
+
data: edge.data,
|
|
1825
|
+
...edge.weight !== void 0 && { weight: edge.weight },
|
|
1826
|
+
...edge.mode !== void 0 && { mode: edge.mode },
|
|
1827
|
+
...source === edge.sourceId && edge.sourcePort !== void 0 && { sourcePort: edge.sourcePort },
|
|
1828
|
+
...target === edge.targetId && edge.targetPort !== void 0 && { targetPort: edge.targetPort }
|
|
1829
|
+
});
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
const leafNodes = graph.nodes.filter((n) => leaves.has(n.id)).map((n) => {
|
|
1833
|
+
const { type, parentId, initialNodeId, ...rest } = n;
|
|
1834
|
+
return rest;
|
|
1835
|
+
});
|
|
1836
|
+
return createGraph({
|
|
1837
|
+
id: graph.id,
|
|
1838
|
+
mode: graph.mode,
|
|
1839
|
+
initialNodeId: graph.initialNodeId ? resolveInitial(graph.initialNodeId) ?? void 0 : void 0,
|
|
1840
|
+
nodes: leafNodes,
|
|
1841
|
+
edges: flatEdges,
|
|
1842
|
+
data: graph.data
|
|
1843
|
+
});
|
|
1844
|
+
}
|
|
1845
|
+
/**
|
|
1846
|
+
* Convert a node to a config, stripping parentId/initialNodeId references
|
|
1847
|
+
* to nodes outside the given set.
|
|
1848
|
+
*/
|
|
1849
|
+
function toScopedNodeConfig(node, nodeIdSet) {
|
|
1850
|
+
const config = toNodeConfig(node);
|
|
1851
|
+
if (nodeIdSet) {
|
|
1852
|
+
if (config.parentId != null && !nodeIdSet.has(config.parentId)) delete config.parentId;
|
|
1853
|
+
if (config.initialNodeId != null && !nodeIdSet.has(config.initialNodeId)) delete config.initialNodeId;
|
|
1854
|
+
}
|
|
1855
|
+
return config;
|
|
1856
|
+
}
|
|
1857
|
+
/**
|
|
1858
|
+
* Returns the induced subgraph containing only the given node IDs
|
|
1859
|
+
* and edges whose endpoints are both in the set.
|
|
1860
|
+
*
|
|
1861
|
+
* Parent references to nodes outside the set are removed.
|
|
1862
|
+
*
|
|
1863
|
+
* @example
|
|
1864
|
+
* ```ts
|
|
1865
|
+
* import { createGraph, getSubgraph } from '@statelyai/graph';
|
|
1866
|
+
*
|
|
1867
|
+
* const graph = createGraph({
|
|
1868
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1869
|
+
* edges: [
|
|
1870
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1871
|
+
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
1872
|
+
* ],
|
|
1873
|
+
* });
|
|
1874
|
+
*
|
|
1875
|
+
* const sub = getSubgraph(graph, ['a', 'b']);
|
|
1876
|
+
* // sub.nodes: [a, b], sub.edges: [ab]
|
|
1877
|
+
* ```
|
|
1878
|
+
*/
|
|
1879
|
+
function getSubgraph(graph, nodeIds) {
|
|
1880
|
+
const nodeIdSet = new Set(nodeIds);
|
|
1881
|
+
return createGraph({
|
|
1882
|
+
id: graph.id,
|
|
1883
|
+
mode: graph.mode,
|
|
1884
|
+
initialNodeId: graph.initialNodeId && nodeIdSet.has(graph.initialNodeId) ? graph.initialNodeId : void 0,
|
|
1885
|
+
nodes: graph.nodes.filter((n) => nodeIdSet.has(n.id)).map((n) => toScopedNodeConfig(n, nodeIdSet)),
|
|
1886
|
+
edges: graph.edges.filter((e) => nodeIdSet.has(e.sourceId) && nodeIdSet.has(e.targetId)).map(toEdgeConfig),
|
|
1887
|
+
data: graph.data
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1890
|
+
/**
|
|
1891
|
+
* Returns a new graph with all edge directions flipped (source ↔ target).
|
|
1892
|
+
* Optionally filters which edges to include.
|
|
1893
|
+
*
|
|
1894
|
+
* @example
|
|
1895
|
+
* ```ts
|
|
1896
|
+
* import { createGraph, reverseGraph } from '@statelyai/graph';
|
|
1897
|
+
*
|
|
1898
|
+
* const graph = createGraph({
|
|
1899
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1900
|
+
* edges: [
|
|
1901
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1902
|
+
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
1903
|
+
* ],
|
|
1904
|
+
* });
|
|
1905
|
+
*
|
|
1906
|
+
* const rev = reverseGraph(graph);
|
|
1907
|
+
* // rev edges: b→a, c→b
|
|
1908
|
+
*
|
|
1909
|
+
* const filtered = reverseGraph(graph, (e) => e.id !== 'bc');
|
|
1910
|
+
* // filtered edges: b→a (only ab reversed, bc excluded)
|
|
1911
|
+
* ```
|
|
1912
|
+
*/
|
|
1913
|
+
function reverseGraph(graph, filterEdge) {
|
|
1914
|
+
const edges = filterEdge ? graph.edges.filter(filterEdge) : graph.edges;
|
|
1915
|
+
return createGraph({
|
|
1916
|
+
id: graph.id,
|
|
1917
|
+
mode: graph.mode,
|
|
1918
|
+
initialNodeId: graph.initialNodeId ?? void 0,
|
|
1919
|
+
nodes: graph.nodes.map((n) => toNodeConfig(n)),
|
|
1920
|
+
edges: edges.map((e) => {
|
|
1921
|
+
const config = toEdgeConfig(e);
|
|
1922
|
+
config.sourceId = e.targetId;
|
|
1923
|
+
config.targetId = e.sourceId;
|
|
1924
|
+
delete config.sourcePort;
|
|
1925
|
+
delete config.targetPort;
|
|
1926
|
+
if (e.targetPort !== void 0) config.sourcePort = e.targetPort;
|
|
1927
|
+
if (e.sourcePort !== void 0) config.targetPort = e.sourcePort;
|
|
1928
|
+
return config;
|
|
1929
|
+
}),
|
|
1930
|
+
data: graph.data
|
|
1931
|
+
});
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1292
1934
|
//#endregion
|
|
1293
1935
|
//#region src/algorithms/traversal.ts
|
|
1294
1936
|
function* bfs(graph, startId) {
|
|
1295
|
-
const
|
|
1296
|
-
const
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1937
|
+
const csr = getCSR(graph);
|
|
1938
|
+
const start = csr.indexOf.get(startId);
|
|
1939
|
+
if (start === void 0) return;
|
|
1940
|
+
const n = csr.ids.length;
|
|
1941
|
+
const visited = new Uint8Array(n);
|
|
1942
|
+
const queue = new Int32Array(n);
|
|
1943
|
+
visited[start] = 1;
|
|
1944
|
+
queue[0] = start;
|
|
1945
|
+
let head = 0;
|
|
1946
|
+
let tail = 1;
|
|
1947
|
+
while (head < tail) {
|
|
1948
|
+
const u = queue[head++];
|
|
1949
|
+
yield graph.nodes[u];
|
|
1950
|
+
for (let a = csr.outOffsets[u]; a < csr.outOffsets[u + 1]; a++) {
|
|
1951
|
+
const v = csr.outTargets[a];
|
|
1952
|
+
if (!visited[v]) {
|
|
1953
|
+
visited[v] = 1;
|
|
1954
|
+
queue[tail++] = v;
|
|
1955
|
+
}
|
|
1307
1956
|
}
|
|
1308
1957
|
}
|
|
1309
1958
|
}
|
|
1310
1959
|
function* dfs(graph, startId) {
|
|
1311
|
-
const
|
|
1312
|
-
const
|
|
1313
|
-
|
|
1960
|
+
const csr = getCSR(graph);
|
|
1961
|
+
const start = csr.indexOf.get(startId);
|
|
1962
|
+
if (start === void 0) return;
|
|
1963
|
+
const n = csr.ids.length;
|
|
1964
|
+
const visited = new Uint8Array(n);
|
|
1965
|
+
const stack = [start];
|
|
1314
1966
|
while (stack.length > 0) {
|
|
1315
|
-
const
|
|
1316
|
-
if (visited
|
|
1317
|
-
visited
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1967
|
+
const u = stack.pop();
|
|
1968
|
+
if (visited[u]) continue;
|
|
1969
|
+
visited[u] = 1;
|
|
1970
|
+
yield graph.nodes[u];
|
|
1971
|
+
for (let a = csr.outOffsets[u]; a < csr.outOffsets[u + 1]; a++) {
|
|
1972
|
+
const v = csr.outTargets[a];
|
|
1973
|
+
if (!visited[v]) stack.push(v);
|
|
1974
|
+
}
|
|
1322
1975
|
}
|
|
1323
1976
|
}
|
|
1324
1977
|
function isAcyclic(graph) {
|
|
1325
|
-
|
|
1978
|
+
const kind = getEffectiveModeKind(graph);
|
|
1979
|
+
if (kind === "mixed") return isAcyclicMixed(graph);
|
|
1980
|
+
if (kind === "non-directed") return isAcyclicUndirected(graph);
|
|
1326
1981
|
const WHITE = 0;
|
|
1327
1982
|
const GRAY = 1;
|
|
1328
1983
|
const BLACK = 2;
|
|
@@ -1341,6 +1996,63 @@ function isAcyclic(graph) {
|
|
|
1341
1996
|
for (const node of graph.nodes) if (color.get(node.id) === WHITE && hasCycle(node.id)) return false;
|
|
1342
1997
|
return true;
|
|
1343
1998
|
}
|
|
1999
|
+
/**
|
|
2000
|
+
* Acyclicity for graphs mixing directed and non-directed edges.
|
|
2001
|
+
*
|
|
2002
|
+
* Polynomial fast paths first: a cycle among directed edges alone, a cycle
|
|
2003
|
+
* among non-directed edges alone (union-find), or all-singleton reachability
|
|
2004
|
+
* SCCs (then no mixed cycle can exist either). Only ambiguous multi-node
|
|
2005
|
+
* SCCs fall back to exact simple-cycle enumeration, restricted to that SCC.
|
|
2006
|
+
*/
|
|
2007
|
+
function isAcyclicMixed(graph) {
|
|
2008
|
+
const idx = getIndex(graph);
|
|
2009
|
+
const WHITE = 0;
|
|
2010
|
+
const GRAY = 1;
|
|
2011
|
+
const BLACK = 2;
|
|
2012
|
+
const color = /* @__PURE__ */ new Map();
|
|
2013
|
+
for (const node of graph.nodes) color.set(node.id, WHITE);
|
|
2014
|
+
const hasDirectedCycle = (id) => {
|
|
2015
|
+
color.set(id, GRAY);
|
|
2016
|
+
for (const eid of idx.outEdges.get(id) ?? []) {
|
|
2017
|
+
const edge = graph.edges[idx.edgeById.get(eid)];
|
|
2018
|
+
if (getEdgeMode(graph, edge) !== "directed") continue;
|
|
2019
|
+
const current = color.get(edge.targetId);
|
|
2020
|
+
if (current === GRAY) return true;
|
|
2021
|
+
if (current === WHITE && hasDirectedCycle(edge.targetId)) return true;
|
|
2022
|
+
}
|
|
2023
|
+
color.set(id, BLACK);
|
|
2024
|
+
return false;
|
|
2025
|
+
};
|
|
2026
|
+
for (const node of graph.nodes) if (color.get(node.id) === WHITE && hasDirectedCycle(node.id)) return false;
|
|
2027
|
+
const parent = /* @__PURE__ */ new Map();
|
|
2028
|
+
const find = (id) => {
|
|
2029
|
+
let root = id;
|
|
2030
|
+
while (parent.get(root) !== root) root = parent.get(root);
|
|
2031
|
+
let cursor = id;
|
|
2032
|
+
while (parent.get(cursor) !== root) {
|
|
2033
|
+
const next = parent.get(cursor);
|
|
2034
|
+
parent.set(cursor, root);
|
|
2035
|
+
cursor = next;
|
|
2036
|
+
}
|
|
2037
|
+
return root;
|
|
2038
|
+
};
|
|
2039
|
+
for (const node of graph.nodes) parent.set(node.id, node.id);
|
|
2040
|
+
for (const edge of graph.edges) {
|
|
2041
|
+
if (getEdgeMode(graph, edge) === "directed") continue;
|
|
2042
|
+
if (edge.sourceId === edge.targetId) return false;
|
|
2043
|
+
const rootA = find(edge.sourceId);
|
|
2044
|
+
const rootB = find(edge.targetId);
|
|
2045
|
+
if (rootA === rootB) return false;
|
|
2046
|
+
parent.set(rootA, rootB);
|
|
2047
|
+
}
|
|
2048
|
+
const multiNodeSccs = getStronglyConnectedComponents(graph).filter((component) => component.length > 1);
|
|
2049
|
+
if (multiNodeSccs.length === 0) return true;
|
|
2050
|
+
for (const component of multiNodeSccs) {
|
|
2051
|
+
const subgraph = getSubgraph(graph, component.map((node) => node.id));
|
|
2052
|
+
for (const _cycle of genCycles(subgraph)) return false;
|
|
2053
|
+
}
|
|
2054
|
+
return true;
|
|
2055
|
+
}
|
|
1344
2056
|
function isAcyclicUndirected(graph) {
|
|
1345
2057
|
const idx = getIndex(graph);
|
|
1346
2058
|
const visited = /* @__PURE__ */ new Set();
|
|
@@ -1368,34 +2080,33 @@ function isAcyclicUndirected(graph) {
|
|
|
1368
2080
|
return true;
|
|
1369
2081
|
}
|
|
1370
2082
|
function getConnectedComponents(graph) {
|
|
1371
|
-
const
|
|
1372
|
-
const
|
|
2083
|
+
const csr = getCSR(graph);
|
|
2084
|
+
const n = csr.ids.length;
|
|
2085
|
+
const visited = new Uint8Array(n);
|
|
2086
|
+
const queue = new Int32Array(n);
|
|
1373
2087
|
const components = [];
|
|
1374
|
-
for (
|
|
1375
|
-
if (visited
|
|
2088
|
+
for (let s = 0; s < n; s++) {
|
|
2089
|
+
if (visited[s]) continue;
|
|
1376
2090
|
const component = [];
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
queue.push(neighborId);
|
|
2091
|
+
visited[s] = 1;
|
|
2092
|
+
queue[0] = s;
|
|
2093
|
+
let head = 0;
|
|
2094
|
+
let tail = 1;
|
|
2095
|
+
while (head < tail) {
|
|
2096
|
+
const u = queue[head++];
|
|
2097
|
+
component.push(graph.nodes[u]);
|
|
2098
|
+
for (let a = csr.outOffsets[u]; a < csr.outOffsets[u + 1]; a++) {
|
|
2099
|
+
const v = csr.outTargets[a];
|
|
2100
|
+
if (!visited[v]) {
|
|
2101
|
+
visited[v] = 1;
|
|
2102
|
+
queue[tail++] = v;
|
|
1390
2103
|
}
|
|
1391
2104
|
}
|
|
1392
|
-
for (
|
|
1393
|
-
const
|
|
1394
|
-
if (
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
visited.add(neighborId);
|
|
1398
|
-
queue.push(neighborId);
|
|
2105
|
+
for (let a = csr.inOffsets[u]; a < csr.inOffsets[u + 1]; a++) {
|
|
2106
|
+
const v = csr.inOrigins[a];
|
|
2107
|
+
if (!visited[v]) {
|
|
2108
|
+
visited[v] = 1;
|
|
2109
|
+
queue[tail++] = v;
|
|
1399
2110
|
}
|
|
1400
2111
|
}
|
|
1401
2112
|
}
|
|
@@ -1403,7 +2114,16 @@ function getConnectedComponents(graph) {
|
|
|
1403
2114
|
}
|
|
1404
2115
|
return components;
|
|
1405
2116
|
}
|
|
2117
|
+
/**
|
|
2118
|
+
* Returns a topological ordering of the graph's nodes, or `null` if no such
|
|
2119
|
+
* ordering exists.
|
|
2120
|
+
*
|
|
2121
|
+
* Any edge whose effective mode (per {@link getEdgeMode}) is not `'directed'`
|
|
2122
|
+
* makes ordering impossible — an undirected/bidirectional edge is mutual
|
|
2123
|
+
* precedence, i.e. a 2-cycle — so the function returns `null`.
|
|
2124
|
+
*/
|
|
1406
2125
|
function getTopologicalSort(graph) {
|
|
2126
|
+
for (const edge of graph.edges) if (getEdgeMode(graph, edge) !== "directed") return null;
|
|
1407
2127
|
const idx = getIndex(graph);
|
|
1408
2128
|
const inDegree = /* @__PURE__ */ new Map();
|
|
1409
2129
|
for (const node of graph.nodes) inDegree.set(node.id, 0);
|
|
@@ -1428,17 +2148,33 @@ function getTopologicalSort(graph) {
|
|
|
1428
2148
|
return result;
|
|
1429
2149
|
}
|
|
1430
2150
|
function hasPath(graph, sourceId, targetId) {
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
2151
|
+
if (sourceId === targetId) return true;
|
|
2152
|
+
const visited = new Set([sourceId]);
|
|
2153
|
+
const queue = [sourceId];
|
|
2154
|
+
while (queue.length > 0) {
|
|
2155
|
+
const id = queue.shift();
|
|
2156
|
+
for (const neighborId of getNeighborIds(graph, id)) {
|
|
2157
|
+
if (neighborId === targetId) return true;
|
|
2158
|
+
if (!visited.has(neighborId)) {
|
|
2159
|
+
visited.add(neighborId);
|
|
2160
|
+
queue.push(neighborId);
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
return false;
|
|
1435
2165
|
}
|
|
1436
2166
|
function isConnected(graph) {
|
|
1437
2167
|
if (graph.nodes.length === 0) return true;
|
|
1438
2168
|
return getConnectedComponents(graph).length <= 1;
|
|
1439
2169
|
}
|
|
2170
|
+
/**
|
|
2171
|
+
* Returns whether the graph is a tree: connected, acyclic, and with exactly
|
|
2172
|
+
* `nodes.length - 1` edges (so directed diamonds and parallel edges are not
|
|
2173
|
+
* trees). Empty and single-node graphs are considered trees.
|
|
2174
|
+
*/
|
|
1440
2175
|
function isTree(graph) {
|
|
1441
|
-
|
|
2176
|
+
if (graph.nodes.length === 0) return true;
|
|
2177
|
+
return graph.edges.length === graph.nodes.length - 1 && isConnected(graph) && isAcyclic(graph);
|
|
1442
2178
|
}
|
|
1443
2179
|
|
|
1444
2180
|
//#endregion
|
|
@@ -1453,7 +2189,7 @@ function getPreorder(graph, opts) {
|
|
|
1453
2189
|
const stack = [startId];
|
|
1454
2190
|
while (stack.length > 0) {
|
|
1455
2191
|
const top = stack[stack.length - 1];
|
|
1456
|
-
const next = getNeighborIds
|
|
2192
|
+
const next = getNeighborIds(graph, top).find((id) => !visited.has(id));
|
|
1457
2193
|
if (next === void 0) {
|
|
1458
2194
|
stack.pop();
|
|
1459
2195
|
continue;
|
|
@@ -1474,7 +2210,7 @@ function getPostorder(graph, opts) {
|
|
|
1474
2210
|
const stack = [startId];
|
|
1475
2211
|
while (stack.length > 0) {
|
|
1476
2212
|
const top = stack[stack.length - 1];
|
|
1477
|
-
const next = getNeighborIds
|
|
2213
|
+
const next = getNeighborIds(graph, top).find((id) => !visited.has(id));
|
|
1478
2214
|
if (next === void 0) {
|
|
1479
2215
|
stack.pop();
|
|
1480
2216
|
const ni = idx.nodeById.get(top);
|
|
@@ -1510,7 +2246,7 @@ function* genPreorders(graph, opts) {
|
|
|
1510
2246
|
let branched = false;
|
|
1511
2247
|
while (dfsStack.length > 0) {
|
|
1512
2248
|
const top = dfsStack[dfsStack.length - 1];
|
|
1513
|
-
const unvisited = getNeighborIds
|
|
2249
|
+
const unvisited = getNeighborIds(graph, top).filter((id) => !visited.has(id));
|
|
1514
2250
|
if (unvisited.length === 0) {
|
|
1515
2251
|
dfsStack.pop();
|
|
1516
2252
|
continue;
|
|
@@ -1548,7 +2284,7 @@ function* genPostorders(graph, opts) {
|
|
|
1548
2284
|
let branched = false;
|
|
1549
2285
|
while (dfsStack.length > 0) {
|
|
1550
2286
|
const top = dfsStack[dfsStack.length - 1];
|
|
1551
|
-
const unvisited = getNeighborIds
|
|
2287
|
+
const unvisited = getNeighborIds(graph, top).filter((id) => !visited.has(id));
|
|
1552
2288
|
if (unvisited.length === 0) {
|
|
1553
2289
|
dfsStack.pop();
|
|
1554
2290
|
const ni = idx.nodeById.get(top);
|
|
@@ -1582,21 +2318,8 @@ function getMinimumSpanningTree(graph, opts) {
|
|
|
1582
2318
|
id: graph.id,
|
|
1583
2319
|
mode: graph.mode,
|
|
1584
2320
|
initialNodeId: graph.initialNodeId ?? void 0,
|
|
1585
|
-
nodes: graph.nodes.map((node) => (
|
|
1586
|
-
|
|
1587
|
-
parentId: node.parentId ?? void 0,
|
|
1588
|
-
initialNodeId: node.initialNodeId ?? void 0,
|
|
1589
|
-
label: node.label,
|
|
1590
|
-
data: node.data
|
|
1591
|
-
})),
|
|
1592
|
-
edges: mstEdges.map((edge) => ({
|
|
1593
|
-
id: edge.id,
|
|
1594
|
-
sourceId: edge.sourceId,
|
|
1595
|
-
targetId: edge.targetId,
|
|
1596
|
-
label: edge.label,
|
|
1597
|
-
data: edge.data,
|
|
1598
|
-
...edge.weight !== void 0 && { weight: edge.weight }
|
|
1599
|
-
}))
|
|
2321
|
+
nodes: graph.nodes.map((node) => toNodeConfig(node)),
|
|
2322
|
+
edges: mstEdges.map((edge) => toEdgeConfig(edge))
|
|
1600
2323
|
});
|
|
1601
2324
|
}
|
|
1602
2325
|
function primMST(graph, getWeight) {
|
|
@@ -1615,26 +2338,28 @@ function primMST(graph, getWeight) {
|
|
|
1615
2338
|
edge
|
|
1616
2339
|
});
|
|
1617
2340
|
}
|
|
1618
|
-
|
|
2341
|
+
for (const eid of idx.inEdges.get(nodeId) ?? []) {
|
|
1619
2342
|
const ai = idx.edgeById.get(eid);
|
|
1620
2343
|
if (ai === void 0) continue;
|
|
1621
2344
|
const edge = graph.edges[ai];
|
|
1622
|
-
if (!inMST.has(edge.sourceId)) candidates.push({
|
|
2345
|
+
if (getEdgeMode(graph, edge) !== "directed" && !inMST.has(edge.sourceId)) candidates.push({
|
|
1623
2346
|
weight: getWeight(edge),
|
|
1624
2347
|
edge
|
|
1625
2348
|
});
|
|
1626
2349
|
}
|
|
1627
2350
|
}
|
|
1628
|
-
const
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
2351
|
+
for (const node of graph.nodes) {
|
|
2352
|
+
if (inMST.has(node.id)) continue;
|
|
2353
|
+
inMST.add(node.id);
|
|
2354
|
+
addEdgesOf(node.id);
|
|
2355
|
+
while (candidates.size > 0 && inMST.size < graph.nodes.length) {
|
|
2356
|
+
const { edge } = candidates.pop();
|
|
2357
|
+
const targetId = getEdgeMode(graph, edge) !== "directed" && inMST.has(edge.targetId) ? edge.sourceId : edge.targetId;
|
|
2358
|
+
if (inMST.has(targetId)) continue;
|
|
2359
|
+
inMST.add(targetId);
|
|
2360
|
+
mstEdges.push(edge);
|
|
2361
|
+
addEdgesOf(targetId);
|
|
2362
|
+
}
|
|
1638
2363
|
}
|
|
1639
2364
|
return mstEdges;
|
|
1640
2365
|
}
|
|
@@ -1672,60 +2397,38 @@ function kruskalMST(graph, getWeight) {
|
|
|
1672
2397
|
function getNodeIds(graph) {
|
|
1673
2398
|
return graph.nodes.map((node) => node.id);
|
|
1674
2399
|
}
|
|
1675
|
-
function getNeighborIds(graph, nodeId) {
|
|
1676
|
-
const idx = getIndex(graph);
|
|
1677
|
-
const neighbors = [];
|
|
1678
|
-
for (const edgeId of idx.outEdges.get(nodeId) ?? []) {
|
|
1679
|
-
const edgeIndex = idx.edgeById.get(edgeId);
|
|
1680
|
-
if (edgeIndex !== void 0) neighbors.push(graph.edges[edgeIndex].targetId);
|
|
1681
|
-
}
|
|
1682
|
-
if (graph.mode !== "directed") for (const edgeId of idx.inEdges.get(nodeId) ?? []) {
|
|
1683
|
-
const edgeIndex = idx.edgeById.get(edgeId);
|
|
1684
|
-
if (edgeIndex !== void 0) neighbors.push(graph.edges[edgeIndex].sourceId);
|
|
1685
|
-
}
|
|
1686
|
-
return neighbors;
|
|
1687
|
-
}
|
|
1688
|
-
function getIncomingIds(graph, nodeId) {
|
|
1689
|
-
const idx = getIndex(graph);
|
|
1690
|
-
const incoming = [];
|
|
1691
|
-
for (const edgeId of idx.inEdges.get(nodeId) ?? []) {
|
|
1692
|
-
const edgeIndex = idx.edgeById.get(edgeId);
|
|
1693
|
-
if (edgeIndex !== void 0) incoming.push(graph.edges[edgeIndex].sourceId);
|
|
1694
|
-
}
|
|
1695
|
-
if (graph.mode !== "directed") for (const edgeId of idx.outEdges.get(nodeId) ?? []) {
|
|
1696
|
-
const edgeIndex = idx.edgeById.get(edgeId);
|
|
1697
|
-
if (edgeIndex !== void 0) incoming.push(graph.edges[edgeIndex].targetId);
|
|
1698
|
-
}
|
|
1699
|
-
return incoming;
|
|
1700
|
-
}
|
|
1701
2400
|
function createEmptyScoreMap(graph) {
|
|
1702
2401
|
return Object.fromEntries(graph.nodes.map((node) => [node.id, 0]));
|
|
1703
2402
|
}
|
|
1704
|
-
function
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
return
|
|
1709
|
-
|
|
1710
|
-
function maxDiff(previous, next) {
|
|
1711
|
-
let diff = 0;
|
|
1712
|
-
for (const key of Object.keys(next)) diff = Math.max(diff, Math.abs((previous[key] ?? 0) - next[key]));
|
|
1713
|
-
return diff;
|
|
2403
|
+
function normalizeTypedVector(values) {
|
|
2404
|
+
let sumOfSquares = 0;
|
|
2405
|
+
for (let i = 0; i < values.length; i++) sumOfSquares += values[i] * values[i];
|
|
2406
|
+
const magnitude = Math.sqrt(sumOfSquares);
|
|
2407
|
+
if (magnitude === 0) return;
|
|
2408
|
+
for (let i = 0; i < values.length; i++) values[i] /= magnitude;
|
|
1714
2409
|
}
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
2410
|
+
/**
|
|
2411
|
+
* BFS hop distances from a start position over the CSR arc snapshot.
|
|
2412
|
+
* `dist[i] === -1` means unreachable. Returns the visit count.
|
|
2413
|
+
*/
|
|
2414
|
+
function bfsDistances(csr, start, dist, queue) {
|
|
2415
|
+
dist.fill(-1);
|
|
2416
|
+
dist[start] = 0;
|
|
2417
|
+
queue[0] = start;
|
|
2418
|
+
let head = 0;
|
|
2419
|
+
let tail = 1;
|
|
2420
|
+
while (head < tail) {
|
|
2421
|
+
const u = queue[head++];
|
|
2422
|
+
const du = dist[u];
|
|
2423
|
+
for (let a = csr.outOffsets[u]; a < csr.outOffsets[u + 1]; a++) {
|
|
2424
|
+
const v = csr.outTargets[a];
|
|
2425
|
+
if (dist[v] === -1) {
|
|
2426
|
+
dist[v] = du + 1;
|
|
2427
|
+
queue[tail++] = v;
|
|
2428
|
+
}
|
|
1726
2429
|
}
|
|
1727
2430
|
}
|
|
1728
|
-
return
|
|
2431
|
+
return tail;
|
|
1729
2432
|
}
|
|
1730
2433
|
/**
|
|
1731
2434
|
* Returns degree centrality scores for all nodes.
|
|
@@ -1740,14 +2443,8 @@ function getReachableDistances(graph, startId) {
|
|
|
1740
2443
|
*/
|
|
1741
2444
|
function getDegreeCentrality(graph) {
|
|
1742
2445
|
const scale = graph.nodes.length > 1 ? 1 / (graph.nodes.length - 1) : 0;
|
|
1743
|
-
const idx = getIndex(graph);
|
|
1744
2446
|
const scores = createEmptyScoreMap(graph);
|
|
1745
|
-
for (const node of graph.nodes)
|
|
1746
|
-
const outDegree = idx.outEdges.get(node.id)?.length ?? 0;
|
|
1747
|
-
const inDegree = idx.inEdges.get(node.id)?.length ?? 0;
|
|
1748
|
-
const degree = graph.mode !== "directed" ? new Set([...idx.outEdges.get(node.id) ?? [], ...idx.inEdges.get(node.id) ?? []]).size : outDegree + inDegree;
|
|
1749
|
-
scores[node.id] = degree * scale;
|
|
1750
|
-
}
|
|
2447
|
+
for (const node of graph.nodes) scores[node.id] = getDegree(graph, node.id) * scale;
|
|
1751
2448
|
return scores;
|
|
1752
2449
|
}
|
|
1753
2450
|
/**
|
|
@@ -1757,9 +2454,8 @@ function getDegreeCentrality(graph) {
|
|
|
1757
2454
|
*/
|
|
1758
2455
|
function getInDegreeCentrality(graph) {
|
|
1759
2456
|
const scale = graph.nodes.length > 1 ? 1 / (graph.nodes.length - 1) : 0;
|
|
1760
|
-
const idx = getIndex(graph);
|
|
1761
2457
|
const scores = createEmptyScoreMap(graph);
|
|
1762
|
-
for (const node of graph.nodes) scores[node.id] = (
|
|
2458
|
+
for (const node of graph.nodes) scores[node.id] = getInDegree(graph, node.id) * scale;
|
|
1763
2459
|
return scores;
|
|
1764
2460
|
}
|
|
1765
2461
|
/**
|
|
@@ -1769,9 +2465,8 @@ function getInDegreeCentrality(graph) {
|
|
|
1769
2465
|
*/
|
|
1770
2466
|
function getOutDegreeCentrality(graph) {
|
|
1771
2467
|
const scale = graph.nodes.length > 1 ? 1 / (graph.nodes.length - 1) : 0;
|
|
1772
|
-
const idx = getIndex(graph);
|
|
1773
2468
|
const scores = createEmptyScoreMap(graph);
|
|
1774
|
-
for (const node of graph.nodes) scores[node.id] = (
|
|
2469
|
+
for (const node of graph.nodes) scores[node.id] = getOutDegree(graph, node.id) * scale;
|
|
1775
2470
|
return scores;
|
|
1776
2471
|
}
|
|
1777
2472
|
/**
|
|
@@ -1782,16 +2477,19 @@ function getOutDegreeCentrality(graph) {
|
|
|
1782
2477
|
*/
|
|
1783
2478
|
function getClosenessCentrality(graph) {
|
|
1784
2479
|
const scores = createEmptyScoreMap(graph);
|
|
1785
|
-
const
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
const
|
|
2480
|
+
const csr = getCSR(graph);
|
|
2481
|
+
const order = csr.ids.length;
|
|
2482
|
+
const dist = new Int32Array(order);
|
|
2483
|
+
const queue = new Int32Array(order);
|
|
2484
|
+
for (let s = 0; s < order; s++) {
|
|
2485
|
+
const visited = bfsDistances(csr, s, dist, queue);
|
|
2486
|
+
const reachable = visited - 1;
|
|
2487
|
+
if (reachable === 0) continue;
|
|
2488
|
+
let totalDistance = 0;
|
|
2489
|
+
for (let k = 0; k < visited; k++) totalDistance += dist[queue[k]];
|
|
1791
2490
|
if (totalDistance === 0) continue;
|
|
1792
|
-
const reachable = distances.size;
|
|
1793
2491
|
const closeness = reachable / totalDistance;
|
|
1794
|
-
scores[
|
|
2492
|
+
scores[csr.ids[s]] = order > 1 ? closeness * (reachable / (order - 1)) : closeness;
|
|
1795
2493
|
}
|
|
1796
2494
|
return scores;
|
|
1797
2495
|
}
|
|
@@ -1802,47 +2500,48 @@ function getClosenessCentrality(graph) {
|
|
|
1802
2500
|
* normalized scores.
|
|
1803
2501
|
*/
|
|
1804
2502
|
function getBetweennessCentrality(graph) {
|
|
1805
|
-
const
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
predecessors.get(neighborId).push(currentId);
|
|
2503
|
+
const csr = getCSR(graph);
|
|
2504
|
+
const n = csr.ids.length;
|
|
2505
|
+
const totals = new Float64Array(n);
|
|
2506
|
+
const sigma = new Float64Array(n);
|
|
2507
|
+
const dist = new Int32Array(n);
|
|
2508
|
+
const delta = new Float64Array(n);
|
|
2509
|
+
const order_ = new Int32Array(n);
|
|
2510
|
+
for (let s = 0; s < n; s++) {
|
|
2511
|
+
sigma.fill(0);
|
|
2512
|
+
dist.fill(-1);
|
|
2513
|
+
delta.fill(0);
|
|
2514
|
+
sigma[s] = 1;
|
|
2515
|
+
dist[s] = 0;
|
|
2516
|
+
order_[0] = s;
|
|
2517
|
+
let head = 0;
|
|
2518
|
+
let tail = 1;
|
|
2519
|
+
while (head < tail) {
|
|
2520
|
+
const u = order_[head++];
|
|
2521
|
+
const du = dist[u];
|
|
2522
|
+
for (let a = csr.outOffsets[u]; a < csr.outOffsets[u + 1]; a++) {
|
|
2523
|
+
const v = csr.outTargets[a];
|
|
2524
|
+
if (dist[v] === -1) {
|
|
2525
|
+
dist[v] = du + 1;
|
|
2526
|
+
order_[tail++] = v;
|
|
1830
2527
|
}
|
|
2528
|
+
if (dist[v] === du + 1) sigma[v] += sigma[u];
|
|
1831
2529
|
}
|
|
1832
2530
|
}
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
const
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
delta.set(predecessorId, delta.get(predecessorId) + contribution);
|
|
2531
|
+
for (let k = tail - 1; k >= 0; k--) {
|
|
2532
|
+
const w = order_[k];
|
|
2533
|
+
const sigmaW = sigma[w];
|
|
2534
|
+
if (sigmaW === 0) continue;
|
|
2535
|
+
const coefficient = (1 + delta[w]) / sigmaW;
|
|
2536
|
+
for (let a = csr.inOffsets[w]; a < csr.inOffsets[w + 1]; a++) {
|
|
2537
|
+
const v = csr.inOrigins[a];
|
|
2538
|
+
if (dist[v] === dist[w] - 1) delta[v] += sigma[v] * coefficient;
|
|
1842
2539
|
}
|
|
1843
|
-
if (
|
|
2540
|
+
if (w !== s) totals[w] += delta[w];
|
|
1844
2541
|
}
|
|
1845
2542
|
}
|
|
2543
|
+
const scores = createEmptyScoreMap(graph);
|
|
2544
|
+
for (let i = 0; i < n; i++) scores[csr.ids[i]] = totals[i];
|
|
1846
2545
|
const order = graph.nodes.length;
|
|
1847
2546
|
if (order <= 2) return scores;
|
|
1848
2547
|
const scale = graph.mode !== "directed" ? 1 / ((order - 1) * (order - 2) / 2) : 1 / ((order - 1) * (order - 2));
|
|
@@ -1864,28 +2563,32 @@ function getPageRank(graph, options) {
|
|
|
1864
2563
|
const maxIterations = options?.maxIterations ?? 100;
|
|
1865
2564
|
const tolerance = options?.tolerance ?? 1e-6;
|
|
1866
2565
|
let scores = Object.fromEntries(nodeIds.map((nodeId) => [nodeId, 1 / nodeIds.length]));
|
|
2566
|
+
const csr = getCSR(graph);
|
|
2567
|
+
const n = csr.ids.length;
|
|
2568
|
+
let current = new Float64Array(n).fill(1 / n);
|
|
2569
|
+
for (let i = 0; i < n; i++) current[i] = scores[csr.ids[i]];
|
|
1867
2570
|
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
1868
|
-
const
|
|
2571
|
+
const next = new Float64Array(n).fill((1 - alpha) / n);
|
|
1869
2572
|
let danglingMass = 0;
|
|
1870
|
-
for (
|
|
1871
|
-
const
|
|
1872
|
-
if (
|
|
1873
|
-
danglingMass +=
|
|
2573
|
+
for (let u = 0; u < n; u++) {
|
|
2574
|
+
const arcCount = csr.outOffsets[u + 1] - csr.outOffsets[u];
|
|
2575
|
+
if (arcCount === 0) {
|
|
2576
|
+
danglingMass += current[u];
|
|
1874
2577
|
continue;
|
|
1875
2578
|
}
|
|
1876
|
-
const share =
|
|
1877
|
-
for (
|
|
2579
|
+
const share = alpha * current[u] / arcCount;
|
|
2580
|
+
for (let a = csr.outOffsets[u]; a < csr.outOffsets[u + 1]; a++) next[csr.outTargets[a]] += share;
|
|
1878
2581
|
}
|
|
1879
2582
|
if (danglingMass > 0) {
|
|
1880
|
-
const share = alpha * danglingMass /
|
|
1881
|
-
for (
|
|
1882
|
-
}
|
|
1883
|
-
if (maxDiff(scores, nextScores) <= tolerance) {
|
|
1884
|
-
scores = nextScores;
|
|
1885
|
-
break;
|
|
2583
|
+
const share = alpha * danglingMass / n;
|
|
2584
|
+
for (let i = 0; i < n; i++) next[i] += share;
|
|
1886
2585
|
}
|
|
1887
|
-
|
|
2586
|
+
let diff = 0;
|
|
2587
|
+
for (let i = 0; i < n; i++) diff = Math.max(diff, Math.abs(current[i] - next[i]));
|
|
2588
|
+
current = next;
|
|
2589
|
+
if (diff <= tolerance) break;
|
|
1888
2590
|
}
|
|
2591
|
+
for (let i = 0; i < n; i++) scores[csr.ids[i]] = current[i];
|
|
1889
2592
|
const total = Object.values(scores).reduce((sum, value) => sum + value, 0);
|
|
1890
2593
|
if (total !== 0) for (const nodeId of nodeIds) scores[nodeId] /= total;
|
|
1891
2594
|
return scores;
|
|
@@ -1896,31 +2599,38 @@ function getPageRank(graph, options) {
|
|
|
1896
2599
|
* Uses power iteration and L2 normalization per iteration.
|
|
1897
2600
|
*/
|
|
1898
2601
|
function getHITS(graph, options) {
|
|
1899
|
-
|
|
1900
|
-
if (nodeIds.length === 0) return {
|
|
2602
|
+
if (getNodeIds(graph).length === 0) return {
|
|
1901
2603
|
hubs: {},
|
|
1902
2604
|
authorities: {}
|
|
1903
2605
|
};
|
|
1904
2606
|
const maxIterations = options?.maxIterations ?? 100;
|
|
1905
2607
|
const tolerance = options?.tolerance ?? 1e-6;
|
|
1906
|
-
|
|
1907
|
-
|
|
2608
|
+
const csr = getCSR(graph);
|
|
2609
|
+
const n = csr.ids.length;
|
|
2610
|
+
let hubs = new Float64Array(n).fill(1);
|
|
2611
|
+
let authorities = new Float64Array(n);
|
|
1908
2612
|
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
1909
|
-
const nextAuthorities =
|
|
1910
|
-
for (
|
|
1911
|
-
|
|
1912
|
-
const nextHubs =
|
|
1913
|
-
for (
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
2613
|
+
const nextAuthorities = new Float64Array(n);
|
|
2614
|
+
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]];
|
|
2615
|
+
normalizeTypedVector(nextAuthorities);
|
|
2616
|
+
const nextHubs = new Float64Array(n);
|
|
2617
|
+
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]];
|
|
2618
|
+
normalizeTypedVector(nextHubs);
|
|
2619
|
+
let diff = 0;
|
|
2620
|
+
for (let i = 0; i < n; i++) diff = Math.max(diff, Math.abs(hubs[i] - nextHubs[i]), Math.abs(authorities[i] - nextAuthorities[i]));
|
|
1917
2621
|
hubs = nextHubs;
|
|
1918
2622
|
authorities = nextAuthorities;
|
|
1919
|
-
if (
|
|
2623
|
+
if (diff <= tolerance) break;
|
|
2624
|
+
}
|
|
2625
|
+
const hubScores = createEmptyScoreMap(graph);
|
|
2626
|
+
const authorityScores = createEmptyScoreMap(graph);
|
|
2627
|
+
for (let i = 0; i < n; i++) {
|
|
2628
|
+
hubScores[csr.ids[i]] = hubs[i];
|
|
2629
|
+
authorityScores[csr.ids[i]] = authorities[i];
|
|
1920
2630
|
}
|
|
1921
2631
|
return {
|
|
1922
|
-
hubs,
|
|
1923
|
-
authorities
|
|
2632
|
+
hubs: hubScores,
|
|
2633
|
+
authorities: authorityScores
|
|
1924
2634
|
};
|
|
1925
2635
|
}
|
|
1926
2636
|
/**
|
|
@@ -1930,20 +2640,24 @@ function getHITS(graph, options) {
|
|
|
1930
2640
|
* undirected adjacency for undirected graphs.
|
|
1931
2641
|
*/
|
|
1932
2642
|
function getEigenvectorCentrality(graph, options) {
|
|
1933
|
-
|
|
1934
|
-
if (nodeIds.length === 0) return {};
|
|
2643
|
+
if (getNodeIds(graph).length === 0) return {};
|
|
1935
2644
|
const maxIterations = options?.maxIterations ?? 100;
|
|
1936
2645
|
const tolerance = options?.tolerance ?? 1e-6;
|
|
1937
|
-
|
|
1938
|
-
|
|
2646
|
+
const csr = getCSR(graph);
|
|
2647
|
+
const n = csr.ids.length;
|
|
2648
|
+
let current = new Float64Array(n).fill(1);
|
|
2649
|
+
normalizeTypedVector(current);
|
|
1939
2650
|
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
1940
|
-
const
|
|
1941
|
-
for (
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
2651
|
+
const next = new Float64Array(n);
|
|
2652
|
+
for (let w = 0; w < n; w++) for (let a = csr.inOffsets[w]; a < csr.inOffsets[w + 1]; a++) next[w] += current[csr.inOrigins[a]];
|
|
2653
|
+
normalizeTypedVector(next);
|
|
2654
|
+
let diff = 0;
|
|
2655
|
+
for (let i = 0; i < n; i++) diff = Math.max(diff, Math.abs(current[i] - next[i]));
|
|
2656
|
+
current = next;
|
|
1945
2657
|
if (diff <= tolerance) break;
|
|
1946
2658
|
}
|
|
2659
|
+
const scores = createEmptyScoreMap(graph);
|
|
2660
|
+
for (let i = 0; i < n; i++) scores[csr.ids[i]] = current[i];
|
|
1947
2661
|
return scores;
|
|
1948
2662
|
}
|
|
1949
2663
|
|
|
@@ -2251,8 +2965,8 @@ function traverseConnectivity(graph, nodeId, parentEdgeId, state) {
|
|
|
2251
2965
|
traverseConnectivity(graph, neighbor.nodeId, neighbor.edgeId, state);
|
|
2252
2966
|
state.low.set(nodeId, Math.min(state.low.get(nodeId), state.low.get(neighbor.nodeId)));
|
|
2253
2967
|
if (state.low.get(neighbor.nodeId) > state.disc.get(nodeId)) state.bridges.add(neighbor.edgeId);
|
|
2254
|
-
if (
|
|
2255
|
-
state.articulationPoints.add(nodeId);
|
|
2968
|
+
if (state.low.get(neighbor.nodeId) >= state.disc.get(nodeId)) {
|
|
2969
|
+
if (parentEdgeId !== null) state.articulationPoints.add(nodeId);
|
|
2256
2970
|
popComponentUntil(state, neighbor.edgeId);
|
|
2257
2971
|
}
|
|
2258
2972
|
} else if (state.disc.get(neighbor.nodeId) < state.disc.get(nodeId)) {
|
|
@@ -2316,14 +3030,33 @@ function getBiconnectedComponents(graph) {
|
|
|
2316
3030
|
//#region src/algorithms/isomorphism.ts
|
|
2317
3031
|
function getDegreeSignature(graph, nodeId) {
|
|
2318
3032
|
const idx = getIndex(graph);
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
3033
|
+
let inDegree = 0;
|
|
3034
|
+
let outDegree = 0;
|
|
3035
|
+
let undirected = 0;
|
|
3036
|
+
const countedNonDirected = /* @__PURE__ */ new Set();
|
|
3037
|
+
for (const eid of idx.outEdges.get(nodeId) ?? []) {
|
|
3038
|
+
const edge = graph.edges[idx.edgeById.get(eid)];
|
|
3039
|
+
if (getEdgeMode(graph, edge) === "directed") outDegree++;
|
|
3040
|
+
else if (!countedNonDirected.has(eid)) {
|
|
3041
|
+
countedNonDirected.add(eid);
|
|
3042
|
+
undirected++;
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
for (const eid of idx.inEdges.get(nodeId) ?? []) {
|
|
3046
|
+
const edge = graph.edges[idx.edgeById.get(eid)];
|
|
3047
|
+
if (getEdgeMode(graph, edge) === "directed") inDegree++;
|
|
3048
|
+
else if (!countedNonDirected.has(eid)) {
|
|
3049
|
+
countedNonDirected.add(eid);
|
|
3050
|
+
undirected++;
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
return `d:${inDegree}:${outDegree}:u:${undirected}`;
|
|
2323
3054
|
}
|
|
2324
3055
|
function getEdgesBetween(graph, sourceId, targetId) {
|
|
2325
|
-
|
|
2326
|
-
|
|
3056
|
+
return graph.edges.filter((edge) => {
|
|
3057
|
+
if (edge.sourceId === sourceId && edge.targetId === targetId) return true;
|
|
3058
|
+
return edge.sourceId === targetId && edge.targetId === sourceId && getEdgeMode(graph, edge) !== "directed";
|
|
3059
|
+
});
|
|
2327
3060
|
}
|
|
2328
3061
|
function edgesAreCompatible(edgesA, edgesB, edgeMatch) {
|
|
2329
3062
|
if (edgesA.length !== edgesB.length) return false;
|
|
@@ -2343,7 +3076,6 @@ function edgesAreCompatible(edgesA, edgesB, edgeMatch) {
|
|
|
2343
3076
|
* node and edge payloads.
|
|
2344
3077
|
*/
|
|
2345
3078
|
function isIsomorphic(graphA, graphB, options) {
|
|
2346
|
-
if (graphA.mode !== graphB.mode) return false;
|
|
2347
3079
|
if (graphA.nodes.length !== graphB.nodes.length) return false;
|
|
2348
3080
|
if (graphA.edges.length !== graphB.edges.length) return false;
|
|
2349
3081
|
const nodeMatch = options?.nodeMatch;
|
|
@@ -2367,6 +3099,7 @@ function isIsomorphic(graphA, graphB, options) {
|
|
|
2367
3099
|
if (usedB.has(nodeB.id)) continue;
|
|
2368
3100
|
if (getDegreeSignature(graphB, nodeB.id) !== signatureA) continue;
|
|
2369
3101
|
if (nodeMatch && !nodeMatch(nodeA, nodeB)) continue;
|
|
3102
|
+
if (!edgesAreCompatible(getEdgesBetween(graphA, nodeA.id, nodeA.id), getEdgesBetween(graphB, nodeB.id, nodeB.id), edgeMatch)) continue;
|
|
2370
3103
|
let compatible = true;
|
|
2371
3104
|
for (const [mappedAId, mappedBId] of mapping.entries()) {
|
|
2372
3105
|
if (!edgesAreCompatible(getEdgesBetween(graphA, nodeA.id, mappedAId), getEdgesBetween(graphB, nodeB.id, mappedBId), edgeMatch)) {
|
|
@@ -2391,4 +3124,404 @@ function isIsomorphic(graphA, graphB, options) {
|
|
|
2391
3124
|
}
|
|
2392
3125
|
|
|
2393
3126
|
//#endregion
|
|
2394
|
-
|
|
3127
|
+
//#region src/algorithms/louvain.ts
|
|
3128
|
+
/**
|
|
3129
|
+
* Returns communities found by the classic two-phase Louvain modularity
|
|
3130
|
+
* optimization (local moving + community aggregation).
|
|
3131
|
+
*
|
|
3132
|
+
* Like the other community algorithms in this library, the graph is treated
|
|
3133
|
+
* as undirected regardless of `graph.mode` or per-edge modes. Parallel edges
|
|
3134
|
+
* have their weights summed; self-loops contribute to a community's internal
|
|
3135
|
+
* weight.
|
|
3136
|
+
*
|
|
3137
|
+
* The implementation is deterministic: nodes are visited in `graph.nodes`
|
|
3138
|
+
* array order and there is no random shuffling, so tie-breaking is
|
|
3139
|
+
* order-dependent but stable across runs.
|
|
3140
|
+
*
|
|
3141
|
+
* Returns communities of node ids, each community sorted lexicographically
|
|
3142
|
+
* and communities sorted by their first id.
|
|
3143
|
+
*
|
|
3144
|
+
* @example
|
|
3145
|
+
* ```ts
|
|
3146
|
+
* const communities = getLouvainCommunities(graph);
|
|
3147
|
+
* // [['a', 'b', 'c'], ['d', 'e', 'f']]
|
|
3148
|
+
* ```
|
|
3149
|
+
*/
|
|
3150
|
+
function getLouvainCommunities(graph, options) {
|
|
3151
|
+
if (graph.nodes.length === 0) return [];
|
|
3152
|
+
const getWeight = options?.getWeight ?? ((edge) => edge.weight ?? 1);
|
|
3153
|
+
const resolution = options?.resolution ?? 1;
|
|
3154
|
+
const maxPasses = options?.maxPasses ?? 10;
|
|
3155
|
+
const nodeIds = graph.nodes.map((node) => node.id);
|
|
3156
|
+
const indexOf = new Map(nodeIds.map((id, i) => [id, i]));
|
|
3157
|
+
let count = nodeIds.length;
|
|
3158
|
+
let links = Array.from({ length: count }, () => /* @__PURE__ */ new Map());
|
|
3159
|
+
let selfLoops = new Array(count).fill(0);
|
|
3160
|
+
for (const edge of graph.edges) {
|
|
3161
|
+
const u = indexOf.get(edge.sourceId);
|
|
3162
|
+
const v = indexOf.get(edge.targetId);
|
|
3163
|
+
if (u === void 0 || v === void 0) continue;
|
|
3164
|
+
const w = getWeight(edge);
|
|
3165
|
+
if (u === v) selfLoops[u] += w;
|
|
3166
|
+
else {
|
|
3167
|
+
links[u].set(v, (links[u].get(v) ?? 0) + w);
|
|
3168
|
+
links[v].set(u, (links[v].get(u) ?? 0) + w);
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
let membership = nodeIds.map((_, i) => i);
|
|
3172
|
+
for (let pass = 0; pass < maxPasses; pass++) {
|
|
3173
|
+
const degree = links.map((neighbors, i) => 2 * selfLoops[i] + [...neighbors.values()].reduce((sum, w) => sum + w, 0));
|
|
3174
|
+
const m2 = degree.reduce((sum, k) => sum + k, 0);
|
|
3175
|
+
if (m2 === 0) break;
|
|
3176
|
+
const communityOf = Array.from({ length: count }, (_, i) => i);
|
|
3177
|
+
const communityTotal = [...degree];
|
|
3178
|
+
let movedAny = false;
|
|
3179
|
+
let movedThisSweep = true;
|
|
3180
|
+
while (movedThisSweep) {
|
|
3181
|
+
movedThisSweep = false;
|
|
3182
|
+
for (let i = 0; i < count; i++) {
|
|
3183
|
+
const current = communityOf[i];
|
|
3184
|
+
const weightTo = /* @__PURE__ */ new Map();
|
|
3185
|
+
for (const [j, w] of links[i]) {
|
|
3186
|
+
const c = communityOf[j];
|
|
3187
|
+
weightTo.set(c, (weightTo.get(c) ?? 0) + w);
|
|
3188
|
+
}
|
|
3189
|
+
communityTotal[current] -= degree[i];
|
|
3190
|
+
const gainOf = (c) => (weightTo.get(c) ?? 0) - resolution * communityTotal[c] * degree[i] / m2;
|
|
3191
|
+
let best = current;
|
|
3192
|
+
let bestGain = gainOf(current);
|
|
3193
|
+
for (const c of weightTo.keys()) {
|
|
3194
|
+
if (c === current) continue;
|
|
3195
|
+
const gain = gainOf(c);
|
|
3196
|
+
if (gain > bestGain + 1e-12) {
|
|
3197
|
+
best = c;
|
|
3198
|
+
bestGain = gain;
|
|
3199
|
+
}
|
|
3200
|
+
}
|
|
3201
|
+
communityTotal[best] += degree[i];
|
|
3202
|
+
if (best !== current) {
|
|
3203
|
+
communityOf[i] = best;
|
|
3204
|
+
movedThisSweep = true;
|
|
3205
|
+
movedAny = true;
|
|
3206
|
+
}
|
|
3207
|
+
}
|
|
3208
|
+
}
|
|
3209
|
+
if (!movedAny) break;
|
|
3210
|
+
const renumber = /* @__PURE__ */ new Map();
|
|
3211
|
+
for (let i = 0; i < count; i++) {
|
|
3212
|
+
const c = communityOf[i];
|
|
3213
|
+
if (!renumber.has(c)) renumber.set(c, renumber.size);
|
|
3214
|
+
}
|
|
3215
|
+
const nextCount = renumber.size;
|
|
3216
|
+
const nextLinks = Array.from({ length: nextCount }, () => /* @__PURE__ */ new Map());
|
|
3217
|
+
const nextSelfLoops = new Array(nextCount).fill(0);
|
|
3218
|
+
for (let i = 0; i < count; i++) {
|
|
3219
|
+
const ci = renumber.get(communityOf[i]);
|
|
3220
|
+
nextSelfLoops[ci] += selfLoops[i];
|
|
3221
|
+
for (const [j, w] of links[i]) {
|
|
3222
|
+
const cj = renumber.get(communityOf[j]);
|
|
3223
|
+
if (ci === cj) nextSelfLoops[ci] += w / 2;
|
|
3224
|
+
else nextLinks[ci].set(cj, (nextLinks[ci].get(cj) ?? 0) + w);
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
membership = membership.map((c) => renumber.get(communityOf[c]));
|
|
3228
|
+
links = nextLinks;
|
|
3229
|
+
selfLoops = nextSelfLoops;
|
|
3230
|
+
count = nextCount;
|
|
3231
|
+
if (nextCount === 1) break;
|
|
3232
|
+
}
|
|
3233
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
3234
|
+
for (let i = 0; i < nodeIds.length; i++) {
|
|
3235
|
+
const c = membership[i];
|
|
3236
|
+
if (!grouped.has(c)) grouped.set(c, []);
|
|
3237
|
+
grouped.get(c).push(nodeIds[i]);
|
|
3238
|
+
}
|
|
3239
|
+
return [...grouped.values()].map((ids) => ids.sort((a, b) => a.localeCompare(b))).sort((a, b) => a[0].localeCompare(b[0]));
|
|
3240
|
+
}
|
|
3241
|
+
|
|
3242
|
+
//#endregion
|
|
3243
|
+
//#region src/algorithms/flow.ts
|
|
3244
|
+
/**
|
|
3245
|
+
* Returns the maximum flow from `from` to `to` using the Edmonds-Karp
|
|
3246
|
+
* algorithm (BFS augmenting paths).
|
|
3247
|
+
*
|
|
3248
|
+
* Directed edges carry capacity from source to target only. Edges whose
|
|
3249
|
+
* effective mode is not `'directed'` (undirected/bidirectional) are modeled
|
|
3250
|
+
* as two independent opposite arcs, each with the edge's full capacity.
|
|
3251
|
+
*
|
|
3252
|
+
* The returned `flows` record maps every edge id to its net flow (positive
|
|
3253
|
+
* in the source→target direction). `cutEdges` is a minimum s-t cut: the
|
|
3254
|
+
* edges crossing from the source side to the sink side of the final
|
|
3255
|
+
* residual graph; the sum of their capacities equals `value`.
|
|
3256
|
+
*
|
|
3257
|
+
* @example
|
|
3258
|
+
* ```ts
|
|
3259
|
+
* const { value, cutEdges } = getMaxFlow(graph, { from: 's', to: 't' });
|
|
3260
|
+
* ```
|
|
3261
|
+
*/
|
|
3262
|
+
function getMaxFlow(graph, options) {
|
|
3263
|
+
const { from, to } = options;
|
|
3264
|
+
const getCapacity = options.getCapacity ?? ((edge) => edge.weight ?? 1);
|
|
3265
|
+
const idx = getIndex(graph);
|
|
3266
|
+
if (!idx.nodeById.has(from)) throw new Error(`getMaxFlow: source node "${from}" not found in graph — pass an existing node id as options.from`);
|
|
3267
|
+
if (!idx.nodeById.has(to)) throw new Error(`getMaxFlow: sink node "${to}" not found in graph — pass an existing node id as options.to`);
|
|
3268
|
+
if (from === to) throw new Error(`getMaxFlow: source and sink are both "${from}" — they must be different nodes`);
|
|
3269
|
+
const arcs = [];
|
|
3270
|
+
const outArcs = /* @__PURE__ */ new Map();
|
|
3271
|
+
for (const node of graph.nodes) outArcs.set(node.id, []);
|
|
3272
|
+
function addArc(u, v, capacity, edgeId, sign) {
|
|
3273
|
+
outArcs.get(u).push(arcs.length);
|
|
3274
|
+
arcs.push({
|
|
3275
|
+
to: v,
|
|
3276
|
+
capacity,
|
|
3277
|
+
flow: 0,
|
|
3278
|
+
edgeId,
|
|
3279
|
+
sign
|
|
3280
|
+
});
|
|
3281
|
+
outArcs.get(v).push(arcs.length);
|
|
3282
|
+
arcs.push({
|
|
3283
|
+
to: u,
|
|
3284
|
+
capacity: 0,
|
|
3285
|
+
flow: 0
|
|
3286
|
+
});
|
|
3287
|
+
}
|
|
3288
|
+
for (const edge of graph.edges) {
|
|
3289
|
+
const capacity = getCapacity(edge);
|
|
3290
|
+
if (capacity < 0) throw new Error(`getMaxFlow: edge "${edge.id}" has negative capacity ${capacity} — capacities must be >= 0; fix edge.weight or provide a non-negative getCapacity`);
|
|
3291
|
+
if (edge.sourceId === edge.targetId) continue;
|
|
3292
|
+
addArc(edge.sourceId, edge.targetId, capacity, edge.id, 1);
|
|
3293
|
+
if (getEdgeMode(graph, edge) !== "directed") addArc(edge.targetId, edge.sourceId, capacity, edge.id, -1);
|
|
3294
|
+
}
|
|
3295
|
+
function residual(arcIndex) {
|
|
3296
|
+
return arcs[arcIndex].capacity - arcs[arcIndex].flow;
|
|
3297
|
+
}
|
|
3298
|
+
let value = 0;
|
|
3299
|
+
while (true) {
|
|
3300
|
+
const parentArc = /* @__PURE__ */ new Map();
|
|
3301
|
+
const queue$1 = [from];
|
|
3302
|
+
const visited = new Set([from]);
|
|
3303
|
+
while (queue$1.length > 0 && !visited.has(to)) {
|
|
3304
|
+
const u = queue$1.shift();
|
|
3305
|
+
for (const ai of outArcs.get(u) ?? []) {
|
|
3306
|
+
const arc = arcs[ai];
|
|
3307
|
+
if (residual(ai) > 0 && !visited.has(arc.to)) {
|
|
3308
|
+
visited.add(arc.to);
|
|
3309
|
+
parentArc.set(arc.to, ai);
|
|
3310
|
+
queue$1.push(arc.to);
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
}
|
|
3314
|
+
if (!visited.has(to)) break;
|
|
3315
|
+
let bottleneck = Infinity;
|
|
3316
|
+
for (let v = to; v !== from;) {
|
|
3317
|
+
const ai = parentArc.get(v);
|
|
3318
|
+
bottleneck = Math.min(bottleneck, residual(ai));
|
|
3319
|
+
v = arcs[ai ^ 1].to;
|
|
3320
|
+
}
|
|
3321
|
+
if (bottleneck === Infinity || bottleneck <= 0) break;
|
|
3322
|
+
for (let v = to; v !== from;) {
|
|
3323
|
+
const ai = parentArc.get(v);
|
|
3324
|
+
arcs[ai].flow += bottleneck;
|
|
3325
|
+
arcs[ai ^ 1].flow -= bottleneck;
|
|
3326
|
+
v = arcs[ai ^ 1].to;
|
|
3327
|
+
}
|
|
3328
|
+
value += bottleneck;
|
|
3329
|
+
}
|
|
3330
|
+
const flows = Object.fromEntries(graph.edges.map((edge) => [edge.id, 0]));
|
|
3331
|
+
for (const arc of arcs) if (arc.edgeId !== void 0 && arc.flow > 0) flows[arc.edgeId] += arc.sign * arc.flow;
|
|
3332
|
+
const sourceSide = new Set([from]);
|
|
3333
|
+
const queue = [from];
|
|
3334
|
+
while (queue.length > 0) {
|
|
3335
|
+
const u = queue.shift();
|
|
3336
|
+
for (const ai of outArcs.get(u) ?? []) {
|
|
3337
|
+
const arc = arcs[ai];
|
|
3338
|
+
if (residual(ai) > 0 && !sourceSide.has(arc.to)) {
|
|
3339
|
+
sourceSide.add(arc.to);
|
|
3340
|
+
queue.push(arc.to);
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
const cutEdgeIds = /* @__PURE__ */ new Set();
|
|
3345
|
+
for (let ai = 0; ai < arcs.length; ai++) {
|
|
3346
|
+
const arc = arcs[ai];
|
|
3347
|
+
if (arc.edgeId === void 0) continue;
|
|
3348
|
+
const arcFrom = arcs[ai ^ 1].to;
|
|
3349
|
+
if (sourceSide.has(arcFrom) && !sourceSide.has(arc.to)) cutEdgeIds.add(arc.edgeId);
|
|
3350
|
+
}
|
|
3351
|
+
const cutEdges = graph.edges.filter((edge) => cutEdgeIds.has(edge.id));
|
|
3352
|
+
return {
|
|
3353
|
+
value,
|
|
3354
|
+
flows,
|
|
3355
|
+
cutEdges
|
|
3356
|
+
};
|
|
3357
|
+
}
|
|
3358
|
+
|
|
3359
|
+
//#endregion
|
|
3360
|
+
//#region src/algorithms/dominators.ts
|
|
3361
|
+
/**
|
|
3362
|
+
* Returns the dominator tree of the graph rooted at `from`, computed with
|
|
3363
|
+
* the Cooper–Harvey–Kennedy iterative algorithm.
|
|
3364
|
+
*
|
|
3365
|
+
* Each reachable node maps to its immediate dominator's id; the root maps
|
|
3366
|
+
* to `null`. Unreachable nodes are omitted. Traversal is mode-aware:
|
|
3367
|
+
* undirected/bidirectional edges are traversable both ways.
|
|
3368
|
+
*
|
|
3369
|
+
* For statecharts this answers "which states must every path from the
|
|
3370
|
+
* initial state pass through to reach this state?" — node `d` dominates
|
|
3371
|
+
* node `n` when every path from the initial state to `n` goes through `d`.
|
|
3372
|
+
*
|
|
3373
|
+
* @example
|
|
3374
|
+
* ```ts
|
|
3375
|
+
* // a→b, a→c, b→d, c→d (diamond)
|
|
3376
|
+
* getDominatorTree(graph, { from: 'a' });
|
|
3377
|
+
* // { a: null, b: 'a', c: 'a', d: 'a' }
|
|
3378
|
+
* ```
|
|
3379
|
+
*/
|
|
3380
|
+
function getDominatorTree(graph, options) {
|
|
3381
|
+
const root = resolveFrom(graph, options);
|
|
3382
|
+
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`);
|
|
3383
|
+
const postorder = [];
|
|
3384
|
+
const visited = new Set([root]);
|
|
3385
|
+
const stack = [{
|
|
3386
|
+
id: root,
|
|
3387
|
+
neighborIndex: 0
|
|
3388
|
+
}];
|
|
3389
|
+
const successors = /* @__PURE__ */ new Map();
|
|
3390
|
+
function getSuccessors(id) {
|
|
3391
|
+
let succ = successors.get(id);
|
|
3392
|
+
if (!succ) {
|
|
3393
|
+
succ = getNeighborEdges(graph, id).map((entry) => entry.neighborId);
|
|
3394
|
+
successors.set(id, succ);
|
|
3395
|
+
}
|
|
3396
|
+
return succ;
|
|
3397
|
+
}
|
|
3398
|
+
while (stack.length > 0) {
|
|
3399
|
+
const frame = stack[stack.length - 1];
|
|
3400
|
+
const succ = getSuccessors(frame.id);
|
|
3401
|
+
if (frame.neighborIndex < succ.length) {
|
|
3402
|
+
const next = succ[frame.neighborIndex++];
|
|
3403
|
+
if (!visited.has(next)) {
|
|
3404
|
+
visited.add(next);
|
|
3405
|
+
stack.push({
|
|
3406
|
+
id: next,
|
|
3407
|
+
neighborIndex: 0
|
|
3408
|
+
});
|
|
3409
|
+
}
|
|
3410
|
+
} else {
|
|
3411
|
+
postorder.push(frame.id);
|
|
3412
|
+
stack.pop();
|
|
3413
|
+
}
|
|
3414
|
+
}
|
|
3415
|
+
const rpo = [...postorder].reverse();
|
|
3416
|
+
const rpoNumber = new Map(rpo.map((id, i) => [id, i]));
|
|
3417
|
+
const predecessors = new Map(rpo.map((id) => [id, []]));
|
|
3418
|
+
for (const id of rpo) for (const succ of getSuccessors(id)) if (visited.has(succ)) predecessors.get(succ).push(id);
|
|
3419
|
+
const idom = /* @__PURE__ */ new Map();
|
|
3420
|
+
idom.set(root, root);
|
|
3421
|
+
function intersect(a, b) {
|
|
3422
|
+
let f1 = a;
|
|
3423
|
+
let f2 = b;
|
|
3424
|
+
while (f1 !== f2) {
|
|
3425
|
+
while (rpoNumber.get(f1) > rpoNumber.get(f2)) f1 = idom.get(f1);
|
|
3426
|
+
while (rpoNumber.get(f2) > rpoNumber.get(f1)) f2 = idom.get(f2);
|
|
3427
|
+
}
|
|
3428
|
+
return f1;
|
|
3429
|
+
}
|
|
3430
|
+
let changed = true;
|
|
3431
|
+
while (changed) {
|
|
3432
|
+
changed = false;
|
|
3433
|
+
for (const id of rpo) {
|
|
3434
|
+
if (id === root) continue;
|
|
3435
|
+
let newIdom;
|
|
3436
|
+
for (const pred of predecessors.get(id)) {
|
|
3437
|
+
if (!idom.has(pred)) continue;
|
|
3438
|
+
newIdom = newIdom === void 0 ? pred : intersect(pred, newIdom);
|
|
3439
|
+
}
|
|
3440
|
+
if (newIdom !== void 0 && idom.get(id) !== newIdom) {
|
|
3441
|
+
idom.set(id, newIdom);
|
|
3442
|
+
changed = true;
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
3445
|
+
}
|
|
3446
|
+
const result = {};
|
|
3447
|
+
for (const id of rpo) result[id] = id === root ? null : idom.get(id);
|
|
3448
|
+
return result;
|
|
3449
|
+
}
|
|
3450
|
+
|
|
3451
|
+
//#endregion
|
|
3452
|
+
//#region src/algorithms/reduction.ts
|
|
3453
|
+
/**
|
|
3454
|
+
* Returns a new graph with all transitively-redundant edges removed (the
|
|
3455
|
+
* transitive reduction). The input graph is not mutated; nodes and surviving
|
|
3456
|
+
* edges keep all of their fields.
|
|
3457
|
+
*
|
|
3458
|
+
* An edge u→v is removed when v is also reachable from u via a path of
|
|
3459
|
+
* length ≥ 2. Exact-duplicate parallel edges u→v collapse to the first one
|
|
3460
|
+
* in `graph.edges` order (a duplicate adds no reachability, so at most one
|
|
3461
|
+
* edge per (u, v) pair survives).
|
|
3462
|
+
*
|
|
3463
|
+
* DAG-only: throws when the graph contains a cycle or any edge whose
|
|
3464
|
+
* effective mode is not `'directed'`.
|
|
3465
|
+
*
|
|
3466
|
+
* @example
|
|
3467
|
+
* ```ts
|
|
3468
|
+
* // a→b, b→c, a→c
|
|
3469
|
+
* const reduced = getTransitiveReduction(graph);
|
|
3470
|
+
* // a→c removed; edges: a→b, b→c
|
|
3471
|
+
* ```
|
|
3472
|
+
*/
|
|
3473
|
+
function getTransitiveReduction(graph) {
|
|
3474
|
+
for (const edge of graph.edges) {
|
|
3475
|
+
const mode = getEdgeMode(graph, edge);
|
|
3476
|
+
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'.`);
|
|
3477
|
+
}
|
|
3478
|
+
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.");
|
|
3479
|
+
const successorsOf = /* @__PURE__ */ new Map();
|
|
3480
|
+
for (const node of graph.nodes) successorsOf.set(node.id, /* @__PURE__ */ new Set());
|
|
3481
|
+
for (const edge of graph.edges) successorsOf.get(edge.sourceId)?.add(edge.targetId);
|
|
3482
|
+
const reach = /* @__PURE__ */ new Map();
|
|
3483
|
+
function getReach(id) {
|
|
3484
|
+
let set = reach.get(id);
|
|
3485
|
+
if (set) return set;
|
|
3486
|
+
set = new Set([id]);
|
|
3487
|
+
reach.set(id, set);
|
|
3488
|
+
for (const succ of successorsOf.get(id) ?? []) for (const reached of getReach(succ)) set.add(reached);
|
|
3489
|
+
return set;
|
|
3490
|
+
}
|
|
3491
|
+
function isRedundant(u, v) {
|
|
3492
|
+
for (const w of successorsOf.get(u)) if (w !== v && getReach(w).has(v)) return true;
|
|
3493
|
+
return false;
|
|
3494
|
+
}
|
|
3495
|
+
const keptPairs = /* @__PURE__ */ new Set();
|
|
3496
|
+
const keptEdges = [];
|
|
3497
|
+
for (const edge of graph.edges) {
|
|
3498
|
+
const pair = `${edge.sourceId}${edge.targetId}`;
|
|
3499
|
+
if (keptPairs.has(pair)) continue;
|
|
3500
|
+
if (isRedundant(edge.sourceId, edge.targetId)) continue;
|
|
3501
|
+
keptPairs.add(pair);
|
|
3502
|
+
keptEdges.push(edge);
|
|
3503
|
+
}
|
|
3504
|
+
return createGraph({
|
|
3505
|
+
id: graph.id,
|
|
3506
|
+
mode: graph.mode,
|
|
3507
|
+
initialNodeId: graph.initialNodeId ?? void 0,
|
|
3508
|
+
data: graph.data ?? void 0,
|
|
3509
|
+
direction: graph.direction,
|
|
3510
|
+
style: graph.style,
|
|
3511
|
+
nodes: graph.nodes.map((node) => {
|
|
3512
|
+
const { type, parentId, initialNodeId, ...rest } = node;
|
|
3513
|
+
return {
|
|
3514
|
+
...rest,
|
|
3515
|
+
parentId: parentId ?? void 0,
|
|
3516
|
+
initialNodeId: initialNodeId ?? void 0
|
|
3517
|
+
};
|
|
3518
|
+
}),
|
|
3519
|
+
edges: keptEdges.map((edge) => {
|
|
3520
|
+
const { type, ...rest } = edge;
|
|
3521
|
+
return rest;
|
|
3522
|
+
})
|
|
3523
|
+
});
|
|
3524
|
+
}
|
|
3525
|
+
|
|
3526
|
+
//#endregion
|
|
3527
|
+
export { joinPaths as $, dfs as A, toEdgeConfig as B, genPostorders as C, getPreorder as D, getPostorders as E, isConnected as F, getAStarPath as G, genCycles as H, isTree as I, getShortestPath as J, getAllPairsShortestPaths as K, flatten as L, getTopologicalSort as M, hasPath as N, getPreorders as O, isAcyclic as P, getStronglyConnectedComponents as Q, getSubgraph as R, getMinimumSpanningTree as S, getPostorder as T, genShortestPaths as U, toNodeConfig as V, genSimplePaths as W, getSimplePath as X, getShortestPaths as Y, getSimplePaths as Z, getEigenvectorCentrality as _, updateEdge as _t, isIsomorphic as a, createGraphEdge as at, getOutDegreeCentrality as b, getBridges as c, createGraphPort as ct, getGreedyModularityCommunities as d, deleteEntities as dt, GraphInstance as et, getLabelPropagationCommunities as f, deleteNode as ft, getDegreeCentrality as g, hasNode as gt, getClosenessCentrality as h, hasEdge as ht, getLouvainCommunities as i, createGraph as it, getConnectedComponents as j, bfs as k, genGirvanNewmanCommunities as l, createVisualGraph as lt, getBetweennessCentrality as m, getNode as mt, getDominatorTree as n, addEntities as nt, getArticulationPoints as o, createGraphFromTransition as ot, getModularity as p, getEdge as pt, getCycles as q, getMaxFlow as r, addNode as rt, getBiconnectedComponents as s, createGraphNode as st, getTransitiveReduction as t, addEdge as tt, getGirvanNewmanCommunities as u, deleteEdge as ut, getHITS as v, updateEntities as vt, genPreorders as w, getPageRank as x, getInDegreeCentrality as y, updateNode as yt, reverseGraph as z };
|