@statelyai/graph 0.13.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 +57 -26
- package/dist/{adjacency-list-Ca0VjKIf.mjs → adjacency-list-GeL1Cu-L.mjs} +5 -3
- package/dist/{algorithms-BlM-qoJb.d.mts → algorithms-CsGNehct.d.mts} +137 -2
- package/dist/{algorithms-BNDQcHU3.mjs → algorithms-DF1pSQGv.mjs} +1494 -357
- package/dist/algorithms.d.mts +2 -2
- package/dist/algorithms.mjs +2 -2
- package/dist/{converter-Dspillnn.mjs → converter-DyCJJfTe.mjs} +2 -2
- package/dist/{edge-list-gKe8-iRa.mjs → edge-list-BcZ0h6zz.mjs} +1 -1
- package/dist/format-support.mjs +67 -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 -60
- package/dist/formats/converter/index.mjs +1 -1
- package/dist/formats/cytoscape/index.d.mts +1 -1
- package/dist/formats/cytoscape/index.mjs +5 -3
- package/dist/formats/d2/index.d.mts +109 -0
- package/dist/formats/d2/index.mjs +1100 -0
- package/dist/formats/d3/index.d.mts +2 -2
- package/dist/formats/d3/index.mjs +5 -3
- package/dist/formats/dot/index.d.mts +1 -1
- package/dist/formats/dot/index.mjs +24 -8
- 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 +23 -16
- package/dist/formats/gexf/index.d.mts +1 -1
- package/dist/formats/gexf/index.mjs +30 -17
- package/dist/formats/gml/index.d.mts +1 -1
- package/dist/formats/gml/index.mjs +22 -13
- package/dist/formats/graphml/index.d.mts +1 -1
- package/dist/formats/graphml/index.mjs +83 -25
- package/dist/formats/jgf/index.d.mts +1 -1
- package/dist/formats/jgf/index.mjs +6 -3
- package/dist/formats/mermaid/index.d.mts +1 -1
- package/dist/formats/mermaid/index.mjs +57 -20
- package/dist/formats/tgf/index.d.mts +1 -1
- package/dist/formats/tgf/index.mjs +2 -2
- package/dist/formats/xyflow/index.d.mts +1 -1
- package/dist/formats/xyflow/index.mjs +33 -6
- package/dist/index-D51lJnt2.d.mts +61 -0
- package/dist/index-DWmo1mIp.d.mts +697 -0
- package/dist/index.d.mts +6 -631
- package/dist/index.mjs +144 -295
- package/dist/mode-D8OnHFBk.mjs +15 -0
- package/dist/queries-BfXeTXRf.d.mts +547 -0
- package/dist/queries-KirMDR7e.mjs +980 -0
- package/dist/queries.d.mts +1 -514
- package/dist/queries.mjs +1 -766
- package/dist/schemas.d.mts +21 -10
- package/dist/schemas.mjs +35 -86
- package/dist/{types-CnZ01raw.d.mts → types-DNYdIU21.d.mts} +83 -11
- package/dist/validate-TtH-x3JV.mjs +190 -0
- package/package.json +14 -3
- package/schemas/edge.schema.json +11 -0
- package/schemas/graph.schema.json +24 -3
- package/schemas/node.schema.json +6 -0
- package/dist/indexing-DUl3kTqm.mjs +0 -137
|
@@ -1,4 +1,5 @@
|
|
|
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
|
+
import { t as getEdgeMode } from "./mode-D8OnHFBk.mjs";
|
|
2
3
|
|
|
3
4
|
//#region src/graph.ts
|
|
4
5
|
/**
|
|
@@ -88,6 +89,7 @@ function createGraphEdge(config) {
|
|
|
88
89
|
};
|
|
89
90
|
if (config.sourcePort !== void 0) edge.sourcePort = config.sourcePort;
|
|
90
91
|
if (config.targetPort !== void 0) edge.targetPort = config.targetPort;
|
|
92
|
+
if (config.mode !== void 0) edge.mode = config.mode;
|
|
91
93
|
if (config.weight !== void 0) edge.weight = config.weight;
|
|
92
94
|
if (config.x !== void 0) edge.x = config.x;
|
|
93
95
|
if (config.y !== void 0) edge.y = config.y;
|
|
@@ -111,7 +113,7 @@ function createGraphEdge(config) {
|
|
|
111
113
|
function createGraph(config) {
|
|
112
114
|
const graph = {
|
|
113
115
|
id: config?.id ?? "",
|
|
114
|
-
|
|
116
|
+
mode: config?.mode ?? "directed",
|
|
115
117
|
initialNodeId: config?.initialNodeId ?? null,
|
|
116
118
|
nodes: (config?.nodes ?? []).map(createGraphNode),
|
|
117
119
|
edges: (config?.edges ?? []).map(createGraphEdge),
|
|
@@ -244,7 +246,7 @@ function createGraphFromTransition(transition, options) {
|
|
|
244
246
|
}
|
|
245
247
|
return createGraph({
|
|
246
248
|
id: options.id ?? "",
|
|
247
|
-
|
|
249
|
+
mode: "directed",
|
|
248
250
|
initialNodeId: initialStateId,
|
|
249
251
|
nodes,
|
|
250
252
|
edges
|
|
@@ -404,15 +406,46 @@ function deleteEdge(graph, id) {
|
|
|
404
406
|
graph.edges = graph.edges.filter((e) => e.id !== id);
|
|
405
407
|
invalidateIndex(graph);
|
|
406
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
|
+
}
|
|
407
438
|
/**
|
|
408
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.
|
|
409
442
|
* @returns The updated node.
|
|
410
443
|
*
|
|
411
444
|
* @example
|
|
412
445
|
* ```ts
|
|
413
446
|
* const graph = createGraph({ nodes: [{ id: 'a', label: 'old' }] });
|
|
414
|
-
* const updated = updateNode(graph, 'a', { label: 'new' });
|
|
415
|
-
* // updated.label === 'new'
|
|
447
|
+
* const updated = updateNode(graph, 'a', { label: 'new', x: 100 });
|
|
448
|
+
* // updated.label === 'new', updated.x === 100
|
|
416
449
|
* ```
|
|
417
450
|
*/
|
|
418
451
|
function updateNode(graph, id, update) {
|
|
@@ -421,24 +454,48 @@ function updateNode(graph, id, update) {
|
|
|
421
454
|
if (arrayIdx === void 0) throw new Error(`Node "${id}" does not exist`);
|
|
422
455
|
if (update.parentId !== void 0 && update.parentId !== null) {
|
|
423
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
|
+
}
|
|
424
465
|
}
|
|
425
|
-
if (update.ports
|
|
466
|
+
if (update.ports != null && update.ports.length > 0) validatePortNames(update.ports);
|
|
426
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
|
+
}
|
|
427
479
|
const oldParentId = node.parentId;
|
|
428
480
|
const updated = {
|
|
429
481
|
...node,
|
|
430
482
|
...update.parentId !== void 0 && { parentId: update.parentId ?? null },
|
|
431
483
|
...update.initialNodeId !== void 0 && { initialNodeId: update.initialNodeId ?? null },
|
|
432
484
|
...update.label !== void 0 && { label: update.label },
|
|
433
|
-
...update.data !== void 0 && { data: update.data }
|
|
434
|
-
...update.ports !== void 0 && { ports: update.ports.map(createGraphPort) }
|
|
485
|
+
...update.data !== void 0 && { data: update.data }
|
|
435
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);
|
|
436
490
|
graph.nodes[arrayIdx] = updated;
|
|
437
491
|
if (update.parentId !== void 0 && updated.parentId !== oldParentId) indexReparentNode(idx, id, oldParentId, updated.parentId);
|
|
438
492
|
return updated;
|
|
439
493
|
}
|
|
440
494
|
/**
|
|
441
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.
|
|
442
499
|
* @returns The updated edge.
|
|
443
500
|
*
|
|
444
501
|
* @example
|
|
@@ -447,8 +504,8 @@ function updateNode(graph, id, update) {
|
|
|
447
504
|
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
448
505
|
* edges: [{ id: 'e1', sourceId: 'a', targetId: 'b', label: 'old' }],
|
|
449
506
|
* });
|
|
450
|
-
* const updated = updateEdge(graph, 'e1', { label: 'new' });
|
|
451
|
-
* // updated.label === 'new'
|
|
507
|
+
* const updated = updateEdge(graph, 'e1', { label: 'new', weight: 2 });
|
|
508
|
+
* // updated.label === 'new', updated.weight === 2
|
|
452
509
|
* ```
|
|
453
510
|
*/
|
|
454
511
|
function updateEdge(graph, id, update) {
|
|
@@ -462,22 +519,28 @@ function updateEdge(graph, id, update) {
|
|
|
462
519
|
const oldTargetId = edge.targetId;
|
|
463
520
|
const effectiveSourceId = update.sourceId ?? edge.sourceId;
|
|
464
521
|
const effectiveTargetId = update.targetId ?? edge.targetId;
|
|
465
|
-
|
|
466
|
-
|
|
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).`);
|
|
467
526
|
}
|
|
468
|
-
if (
|
|
469
|
-
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).`);
|
|
470
529
|
}
|
|
471
530
|
const updated = {
|
|
472
531
|
...edge,
|
|
473
532
|
...update.sourceId !== void 0 && { sourceId: update.sourceId },
|
|
474
533
|
...update.targetId !== void 0 && { targetId: update.targetId },
|
|
475
534
|
...update.label !== void 0 && { label: update.label },
|
|
476
|
-
...update.data !== void 0 && { data: update.data }
|
|
477
|
-
...update.sourcePort !== void 0 && { sourcePort: update.sourcePort },
|
|
478
|
-
...update.targetPort !== void 0 && { targetPort: update.targetPort }
|
|
535
|
+
...update.data !== void 0 && { data: update.data }
|
|
479
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);
|
|
480
542
|
graph.edges[arrayIdx] = updated;
|
|
543
|
+
if (update.mode !== void 0 || update.weight !== void 0) touchIndex(idx);
|
|
481
544
|
if (updated.sourceId !== oldSourceId || updated.targetId !== oldTargetId) indexUpdateEdgeEndpoints(idx, id, oldSourceId, oldTargetId, updated.sourceId, updated.targetId);
|
|
482
545
|
return updated;
|
|
483
546
|
}
|
|
@@ -582,8 +645,9 @@ var GraphInstance = class GraphInstance {
|
|
|
582
645
|
get id() {
|
|
583
646
|
return this.graph.id;
|
|
584
647
|
}
|
|
585
|
-
|
|
586
|
-
|
|
648
|
+
/** Default directedness for all edges. */
|
|
649
|
+
get mode() {
|
|
650
|
+
return this.graph.mode;
|
|
587
651
|
}
|
|
588
652
|
get nodes() {
|
|
589
653
|
return this.graph.nodes;
|
|
@@ -663,6 +727,9 @@ var MinPriorityQueue = class {
|
|
|
663
727
|
this.items.push(item);
|
|
664
728
|
this.bubbleUp(this.items.length - 1);
|
|
665
729
|
}
|
|
730
|
+
peek() {
|
|
731
|
+
return this.items[0];
|
|
732
|
+
}
|
|
666
733
|
pop() {
|
|
667
734
|
if (this.items.length === 0) return void 0;
|
|
668
735
|
const first = this.items[0];
|
|
@@ -696,16 +763,35 @@ var MinPriorityQueue = class {
|
|
|
696
763
|
}
|
|
697
764
|
}
|
|
698
765
|
};
|
|
699
|
-
|
|
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) {
|
|
700
784
|
const idx = getIndex(graph);
|
|
701
785
|
const ids = [];
|
|
702
786
|
for (const eid of idx.outEdges.get(nodeId) ?? []) {
|
|
703
787
|
const ai = idx.edgeById.get(eid);
|
|
704
788
|
if (ai !== void 0) ids.push(graph.edges[ai].targetId);
|
|
705
789
|
}
|
|
706
|
-
|
|
790
|
+
for (const eid of idx.inEdges.get(nodeId) ?? []) {
|
|
707
791
|
const ai = idx.edgeById.get(eid);
|
|
708
|
-
if (ai
|
|
792
|
+
if (ai === void 0) continue;
|
|
793
|
+
const edge = graph.edges[ai];
|
|
794
|
+
if (getEdgeMode(graph, edge) !== "directed") ids.push(edge.sourceId);
|
|
709
795
|
}
|
|
710
796
|
return ids;
|
|
711
797
|
}
|
|
@@ -736,11 +822,11 @@ function getNeighborEdges(graph, nodeId) {
|
|
|
736
822
|
});
|
|
737
823
|
}
|
|
738
824
|
}
|
|
739
|
-
|
|
825
|
+
for (const eid of idx.inEdges.get(nodeId) ?? []) {
|
|
740
826
|
const ai = idx.edgeById.get(eid);
|
|
741
827
|
if (ai !== void 0) {
|
|
742
828
|
const edge = graph.edges[ai];
|
|
743
|
-
result.push({
|
|
829
|
+
if (getEdgeMode(graph, edge) !== "directed") result.push({
|
|
744
830
|
neighborId: edge.sourceId,
|
|
745
831
|
edge
|
|
746
832
|
});
|
|
@@ -774,67 +860,181 @@ function getNeighborEdgesAll(graph, nodeId) {
|
|
|
774
860
|
return result;
|
|
775
861
|
}
|
|
776
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
|
+
|
|
777
955
|
//#endregion
|
|
778
956
|
//#region src/algorithms/paths.ts
|
|
779
|
-
function computeShortestDistances(graph, sourceId, getWeight, algorithm) {
|
|
957
|
+
function computeShortestDistances(graph, sourceId, getWeight, algorithm, stopAtId) {
|
|
780
958
|
if (algorithm === "bellman-ford") return bellmanFord(graph, sourceId, getWeight);
|
|
781
959
|
const dist = /* @__PURE__ */ new Map();
|
|
782
960
|
const prev = /* @__PURE__ */ new Map();
|
|
783
961
|
dist.set(sourceId, 0);
|
|
784
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.");
|
|
785
977
|
if (!getWeight && !graph.edges.some((edge) => edge.weight !== void 0)) {
|
|
786
|
-
const queue =
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
edge
|
|
803
|
-
});
|
|
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]);
|
|
804
994
|
}
|
|
805
995
|
}
|
|
806
996
|
} else {
|
|
807
997
|
const effectiveWeight = getWeight ?? ((edge) => edge.weight ?? 1);
|
|
808
|
-
const visited =
|
|
998
|
+
const visited = new Uint8Array(n);
|
|
809
999
|
const pq = new MinPriorityQueue((a, b) => a.dist - b.dist);
|
|
810
1000
|
pq.push({
|
|
811
|
-
|
|
1001
|
+
pos: source,
|
|
812
1002
|
dist: 0
|
|
813
1003
|
});
|
|
814
1004
|
while (pq.size > 0) {
|
|
815
|
-
const {
|
|
816
|
-
if (visited
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
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]];
|
|
827
1019
|
pq.push({
|
|
828
|
-
|
|
1020
|
+
pos: v,
|
|
829
1021
|
dist: nextDistance
|
|
830
1022
|
});
|
|
831
|
-
} else if (
|
|
832
|
-
from: id,
|
|
833
|
-
edge
|
|
834
|
-
});
|
|
1023
|
+
} else if (nextDistance === distArr[v] && distArr[v] !== Infinity) prevArr[v].push(u, csr.outEdgeIndex[a]);
|
|
835
1024
|
}
|
|
836
1025
|
}
|
|
837
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
|
+
}
|
|
838
1038
|
return {
|
|
839
1039
|
dist,
|
|
840
1040
|
prev
|
|
@@ -844,7 +1044,6 @@ function bellmanFord(graph, sourceId, getWeight) {
|
|
|
844
1044
|
const dist = /* @__PURE__ */ new Map();
|
|
845
1045
|
const prev = /* @__PURE__ */ new Map();
|
|
846
1046
|
const effectiveWeight = getWeight ?? ((edge) => edge.weight ?? 1);
|
|
847
|
-
const isUndirected = graph.type === "undirected";
|
|
848
1047
|
for (const node of graph.nodes) {
|
|
849
1048
|
dist.set(node.id, Infinity);
|
|
850
1049
|
prev.set(node.id, []);
|
|
@@ -857,7 +1056,7 @@ function bellmanFord(graph, sourceId, getWeight) {
|
|
|
857
1056
|
toId: edge.targetId,
|
|
858
1057
|
edge
|
|
859
1058
|
});
|
|
860
|
-
if (
|
|
1059
|
+
if (getEdgeMode(graph, edge) !== "directed") directedEdges.push({
|
|
861
1060
|
fromId: edge.targetId,
|
|
862
1061
|
toId: edge.sourceId,
|
|
863
1062
|
edge
|
|
@@ -901,7 +1100,7 @@ function bellmanFord(graph, sourceId, getWeight) {
|
|
|
901
1100
|
prev
|
|
902
1101
|
};
|
|
903
1102
|
}
|
|
904
|
-
function* reconstructPaths(graph, prev, sourceNode, targetId) {
|
|
1103
|
+
function* reconstructPaths(graph, prev, sourceNode, targetId, onPath = /* @__PURE__ */ new Set()) {
|
|
905
1104
|
if (targetId === sourceNode.id) {
|
|
906
1105
|
yield {
|
|
907
1106
|
source: sourceNode,
|
|
@@ -913,18 +1112,23 @@ function* reconstructPaths(graph, prev, sourceNode, targetId) {
|
|
|
913
1112
|
if (!predecessors || predecessors.length === 0) return;
|
|
914
1113
|
const targetNi = getIndex(graph).nodeById.get(targetId);
|
|
915
1114
|
const targetNode = targetNi !== void 0 ? graph.nodes[targetNi] : graph.nodes.find((node) => node.id === targetId);
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
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);
|
|
923
1127
|
}
|
|
924
1128
|
function* genShortestPaths(graph, opts) {
|
|
925
1129
|
const idx = getIndex(graph);
|
|
926
1130
|
const sourceId = resolveFrom(graph, opts);
|
|
927
|
-
const { dist, prev } = computeShortestDistances(graph, sourceId, opts?.getWeight, opts?.algorithm);
|
|
1131
|
+
const { dist, prev } = computeShortestDistances(graph, sourceId, opts?.getWeight, opts?.algorithm, opts?.to);
|
|
928
1132
|
const targets = opts?.to ? [opts.to].filter((id) => dist.has(id)) : [...dist.keys()].filter((id) => id !== sourceId);
|
|
929
1133
|
const sourceNi = idx.nodeById.get(sourceId);
|
|
930
1134
|
const sourceNode = sourceNi !== void 0 ? graph.nodes[sourceNi] : graph.nodes.find((node) => node.id === sourceId);
|
|
@@ -934,8 +1138,161 @@ function getShortestPaths(graph, opts) {
|
|
|
934
1138
|
return [...genShortestPaths(graph, opts)];
|
|
935
1139
|
}
|
|
936
1140
|
function getShortestPath(graph, opts) {
|
|
1141
|
+
if (opts.algorithm !== "bellman-ford") return bidirectionalShortestPath(graph, resolveFrom(graph, opts), opts.to, opts.getWeight);
|
|
937
1142
|
for (const path of genShortestPaths(graph, opts)) return path;
|
|
938
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
|
+
}
|
|
939
1296
|
function getSimplePaths(graph, opts) {
|
|
940
1297
|
return [...genSimplePaths(graph, opts)];
|
|
941
1298
|
}
|
|
@@ -993,15 +1350,10 @@ function getStronglyConnectedComponents(graph) {
|
|
|
993
1350
|
indexCounter++;
|
|
994
1351
|
stack.push(id);
|
|
995
1352
|
onStack.add(id);
|
|
996
|
-
for (const
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
if (!nodeIndex.has(neighborId)) {
|
|
1001
|
-
strongconnect(neighborId);
|
|
1002
|
-
lowlink.set(id, Math.min(lowlink.get(id), lowlink.get(neighborId)));
|
|
1003
|
-
} else if (onStack.has(neighborId)) lowlink.set(id, Math.min(lowlink.get(id), nodeIndex.get(neighborId)));
|
|
1004
|
-
}
|
|
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)));
|
|
1005
1357
|
if (lowlink.get(id) === nodeIndex.get(id)) {
|
|
1006
1358
|
const component = [];
|
|
1007
1359
|
let neighborId;
|
|
@@ -1021,7 +1373,9 @@ function getCycles(graph) {
|
|
|
1021
1373
|
return [...genCycles(graph)];
|
|
1022
1374
|
}
|
|
1023
1375
|
function* genCycles(graph) {
|
|
1024
|
-
|
|
1376
|
+
const kind = getEffectiveModeKind(graph);
|
|
1377
|
+
if (kind === "mixed") yield* genCyclesMixed(graph);
|
|
1378
|
+
else if (kind === "non-directed") yield* genCyclesUndirected(graph);
|
|
1025
1379
|
else yield* genCyclesDirected(graph);
|
|
1026
1380
|
}
|
|
1027
1381
|
function* genCyclesDirected(graph) {
|
|
@@ -1077,17 +1431,14 @@ function* genCyclesUndirected(graph) {
|
|
|
1077
1431
|
const startNi = idx.nodeById.get(startId);
|
|
1078
1432
|
const startNode = graph.nodes[startNi];
|
|
1079
1433
|
const found = [];
|
|
1080
|
-
function dfsFind(currentId,
|
|
1434
|
+
function dfsFind(currentId, arrivalEdgeId) {
|
|
1081
1435
|
visited.add(currentId);
|
|
1082
1436
|
for (const { neighborId, edge } of getNeighborEdgesAll(graph, currentId)) {
|
|
1083
|
-
if (
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
const innerIds = steps.map((step) => step.node.id).sort().join(",");
|
|
1089
|
-
if (!seen.has(innerIds)) {
|
|
1090
|
-
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);
|
|
1091
1442
|
found.push({
|
|
1092
1443
|
source: startNode,
|
|
1093
1444
|
steps: [...steps, {
|
|
@@ -1102,7 +1453,7 @@ function* genCyclesUndirected(graph) {
|
|
|
1102
1453
|
edge,
|
|
1103
1454
|
node: graph.nodes[ni]
|
|
1104
1455
|
});
|
|
1105
|
-
dfsFind(neighborId,
|
|
1456
|
+
dfsFind(neighborId, edge.id);
|
|
1106
1457
|
steps.pop();
|
|
1107
1458
|
}
|
|
1108
1459
|
}
|
|
@@ -1112,6 +1463,59 @@ function* genCyclesUndirected(graph) {
|
|
|
1112
1463
|
yield* found;
|
|
1113
1464
|
}
|
|
1114
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
|
+
}
|
|
1115
1519
|
function getAllPairsShortestPaths(graph, opts) {
|
|
1116
1520
|
const algorithm = opts?.algorithm ?? "dijkstra";
|
|
1117
1521
|
if (algorithm === "floyd-warshall") return floydWarshallAllPaths(graph, opts?.getWeight);
|
|
@@ -1160,7 +1564,7 @@ function floydWarshallAllPaths(graph, getWeight) {
|
|
|
1160
1564
|
from: source,
|
|
1161
1565
|
edge
|
|
1162
1566
|
});
|
|
1163
|
-
if (graph
|
|
1567
|
+
if (getEdgeMode(graph, edge) !== "directed") {
|
|
1164
1568
|
if (edgeWeight < dist[target][source]) {
|
|
1165
1569
|
dist[target][source] = edgeWeight;
|
|
1166
1570
|
prev[target][source] = [{
|
|
@@ -1183,6 +1587,7 @@ function floydWarshallAllPaths(graph, getWeight) {
|
|
|
1183
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 });
|
|
1184
1588
|
}
|
|
1185
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.`);
|
|
1186
1591
|
const results = [];
|
|
1187
1592
|
for (let i = 0; i < nodeCount; i++) {
|
|
1188
1593
|
const sourceNi = idx.nodeById.get(nodeIds[i]);
|
|
@@ -1229,48 +1634,53 @@ function getAStarPath(graph, opts) {
|
|
|
1229
1634
|
source: graph.nodes[sourceNi],
|
|
1230
1635
|
steps: []
|
|
1231
1636
|
};
|
|
1232
|
-
const
|
|
1233
|
-
const
|
|
1234
|
-
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);
|
|
1235
1645
|
const openSet = new MinPriorityQueue((a, b) => a.f - b.f);
|
|
1236
|
-
|
|
1646
|
+
assertNoNegativeWeights(graph, csr, opts.getWeight, "A*", "Use getShortestPath with { algorithm: 'bellman-ford' } instead.");
|
|
1647
|
+
gScore[source] = 0;
|
|
1237
1648
|
openSet.push({
|
|
1238
|
-
|
|
1649
|
+
pos: source,
|
|
1239
1650
|
f: heuristic(sourceId)
|
|
1240
1651
|
});
|
|
1241
1652
|
while (openSet.size > 0) {
|
|
1242
|
-
const {
|
|
1243
|
-
if (
|
|
1244
|
-
if (
|
|
1653
|
+
const { pos: current } = openSet.pop();
|
|
1654
|
+
if (closed[current]) continue;
|
|
1655
|
+
if (current === target) {
|
|
1245
1656
|
const steps = [];
|
|
1246
|
-
let
|
|
1247
|
-
while (
|
|
1248
|
-
const previous = cameFrom.get(current);
|
|
1249
|
-
const ni = idx.nodeById.get(current);
|
|
1657
|
+
let cursor = target;
|
|
1658
|
+
while (cursor !== source) {
|
|
1250
1659
|
steps.unshift({
|
|
1251
|
-
edge:
|
|
1252
|
-
node: graph.nodes[
|
|
1660
|
+
edge: graph.edges[cameFromEdge[cursor]],
|
|
1661
|
+
node: graph.nodes[cursor]
|
|
1253
1662
|
});
|
|
1254
|
-
|
|
1663
|
+
cursor = cameFromPos[cursor];
|
|
1255
1664
|
}
|
|
1256
1665
|
return {
|
|
1257
1666
|
source: graph.nodes[sourceNi],
|
|
1258
1667
|
steps
|
|
1259
1668
|
};
|
|
1260
1669
|
}
|
|
1261
|
-
|
|
1262
|
-
for (
|
|
1263
|
-
|
|
1264
|
-
const
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
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;
|
|
1271
1681
|
openSet.push({
|
|
1272
|
-
|
|
1273
|
-
f: tentativeScore + heuristic(
|
|
1682
|
+
pos: neighbor,
|
|
1683
|
+
f: tentativeScore + heuristic(csr.ids[neighbor])
|
|
1274
1684
|
});
|
|
1275
1685
|
}
|
|
1276
1686
|
}
|
|
@@ -1285,40 +1695,289 @@ function joinPaths(headPath, tailPath) {
|
|
|
1285
1695
|
};
|
|
1286
1696
|
}
|
|
1287
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
|
+
|
|
1288
1934
|
//#endregion
|
|
1289
1935
|
//#region src/algorithms/traversal.ts
|
|
1290
1936
|
function* bfs(graph, startId) {
|
|
1291
|
-
const
|
|
1292
|
-
const
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
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
|
+
}
|
|
1303
1956
|
}
|
|
1304
1957
|
}
|
|
1305
1958
|
}
|
|
1306
1959
|
function* dfs(graph, startId) {
|
|
1307
|
-
const
|
|
1308
|
-
const
|
|
1309
|
-
|
|
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];
|
|
1310
1966
|
while (stack.length > 0) {
|
|
1311
|
-
const
|
|
1312
|
-
if (visited
|
|
1313
|
-
visited
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
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
|
+
}
|
|
1318
1975
|
}
|
|
1319
1976
|
}
|
|
1320
1977
|
function isAcyclic(graph) {
|
|
1321
|
-
|
|
1978
|
+
const kind = getEffectiveModeKind(graph);
|
|
1979
|
+
if (kind === "mixed") return isAcyclicMixed(graph);
|
|
1980
|
+
if (kind === "non-directed") return isAcyclicUndirected(graph);
|
|
1322
1981
|
const WHITE = 0;
|
|
1323
1982
|
const GRAY = 1;
|
|
1324
1983
|
const BLACK = 2;
|
|
@@ -1337,6 +1996,63 @@ function isAcyclic(graph) {
|
|
|
1337
1996
|
for (const node of graph.nodes) if (color.get(node.id) === WHITE && hasCycle(node.id)) return false;
|
|
1338
1997
|
return true;
|
|
1339
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
|
+
}
|
|
1340
2056
|
function isAcyclicUndirected(graph) {
|
|
1341
2057
|
const idx = getIndex(graph);
|
|
1342
2058
|
const visited = /* @__PURE__ */ new Set();
|
|
@@ -1364,34 +2080,33 @@ function isAcyclicUndirected(graph) {
|
|
|
1364
2080
|
return true;
|
|
1365
2081
|
}
|
|
1366
2082
|
function getConnectedComponents(graph) {
|
|
1367
|
-
const
|
|
1368
|
-
const
|
|
2083
|
+
const csr = getCSR(graph);
|
|
2084
|
+
const n = csr.ids.length;
|
|
2085
|
+
const visited = new Uint8Array(n);
|
|
2086
|
+
const queue = new Int32Array(n);
|
|
1369
2087
|
const components = [];
|
|
1370
|
-
for (
|
|
1371
|
-
if (visited
|
|
2088
|
+
for (let s = 0; s < n; s++) {
|
|
2089
|
+
if (visited[s]) continue;
|
|
1372
2090
|
const component = [];
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
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;
|
|
1386
2103
|
}
|
|
1387
2104
|
}
|
|
1388
|
-
for (
|
|
1389
|
-
const
|
|
1390
|
-
if (
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
visited.add(neighborId);
|
|
1394
|
-
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;
|
|
1395
2110
|
}
|
|
1396
2111
|
}
|
|
1397
2112
|
}
|
|
@@ -1399,7 +2114,16 @@ function getConnectedComponents(graph) {
|
|
|
1399
2114
|
}
|
|
1400
2115
|
return components;
|
|
1401
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
|
+
*/
|
|
1402
2125
|
function getTopologicalSort(graph) {
|
|
2126
|
+
for (const edge of graph.edges) if (getEdgeMode(graph, edge) !== "directed") return null;
|
|
1403
2127
|
const idx = getIndex(graph);
|
|
1404
2128
|
const inDegree = /* @__PURE__ */ new Map();
|
|
1405
2129
|
for (const node of graph.nodes) inDegree.set(node.id, 0);
|
|
@@ -1424,17 +2148,33 @@ function getTopologicalSort(graph) {
|
|
|
1424
2148
|
return result;
|
|
1425
2149
|
}
|
|
1426
2150
|
function hasPath(graph, sourceId, targetId) {
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
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;
|
|
1431
2165
|
}
|
|
1432
2166
|
function isConnected(graph) {
|
|
1433
2167
|
if (graph.nodes.length === 0) return true;
|
|
1434
2168
|
return getConnectedComponents(graph).length <= 1;
|
|
1435
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
|
+
*/
|
|
1436
2175
|
function isTree(graph) {
|
|
1437
|
-
|
|
2176
|
+
if (graph.nodes.length === 0) return true;
|
|
2177
|
+
return graph.edges.length === graph.nodes.length - 1 && isConnected(graph) && isAcyclic(graph);
|
|
1438
2178
|
}
|
|
1439
2179
|
|
|
1440
2180
|
//#endregion
|
|
@@ -1449,7 +2189,7 @@ function getPreorder(graph, opts) {
|
|
|
1449
2189
|
const stack = [startId];
|
|
1450
2190
|
while (stack.length > 0) {
|
|
1451
2191
|
const top = stack[stack.length - 1];
|
|
1452
|
-
const next = getNeighborIds
|
|
2192
|
+
const next = getNeighborIds(graph, top).find((id) => !visited.has(id));
|
|
1453
2193
|
if (next === void 0) {
|
|
1454
2194
|
stack.pop();
|
|
1455
2195
|
continue;
|
|
@@ -1470,7 +2210,7 @@ function getPostorder(graph, opts) {
|
|
|
1470
2210
|
const stack = [startId];
|
|
1471
2211
|
while (stack.length > 0) {
|
|
1472
2212
|
const top = stack[stack.length - 1];
|
|
1473
|
-
const next = getNeighborIds
|
|
2213
|
+
const next = getNeighborIds(graph, top).find((id) => !visited.has(id));
|
|
1474
2214
|
if (next === void 0) {
|
|
1475
2215
|
stack.pop();
|
|
1476
2216
|
const ni = idx.nodeById.get(top);
|
|
@@ -1506,7 +2246,7 @@ function* genPreorders(graph, opts) {
|
|
|
1506
2246
|
let branched = false;
|
|
1507
2247
|
while (dfsStack.length > 0) {
|
|
1508
2248
|
const top = dfsStack[dfsStack.length - 1];
|
|
1509
|
-
const unvisited = getNeighborIds
|
|
2249
|
+
const unvisited = getNeighborIds(graph, top).filter((id) => !visited.has(id));
|
|
1510
2250
|
if (unvisited.length === 0) {
|
|
1511
2251
|
dfsStack.pop();
|
|
1512
2252
|
continue;
|
|
@@ -1544,7 +2284,7 @@ function* genPostorders(graph, opts) {
|
|
|
1544
2284
|
let branched = false;
|
|
1545
2285
|
while (dfsStack.length > 0) {
|
|
1546
2286
|
const top = dfsStack[dfsStack.length - 1];
|
|
1547
|
-
const unvisited = getNeighborIds
|
|
2287
|
+
const unvisited = getNeighborIds(graph, top).filter((id) => !visited.has(id));
|
|
1548
2288
|
if (unvisited.length === 0) {
|
|
1549
2289
|
dfsStack.pop();
|
|
1550
2290
|
const ni = idx.nodeById.get(top);
|
|
@@ -1576,23 +2316,10 @@ function getMinimumSpanningTree(graph, opts) {
|
|
|
1576
2316
|
const mstEdges = algorithm === "kruskal" ? kruskalMST(graph, getWeight) : primMST(graph, getWeight);
|
|
1577
2317
|
return createGraph({
|
|
1578
2318
|
id: graph.id,
|
|
1579
|
-
|
|
2319
|
+
mode: graph.mode,
|
|
1580
2320
|
initialNodeId: graph.initialNodeId ?? void 0,
|
|
1581
|
-
nodes: graph.nodes.map((node) => (
|
|
1582
|
-
|
|
1583
|
-
parentId: node.parentId ?? void 0,
|
|
1584
|
-
initialNodeId: node.initialNodeId ?? void 0,
|
|
1585
|
-
label: node.label,
|
|
1586
|
-
data: node.data
|
|
1587
|
-
})),
|
|
1588
|
-
edges: mstEdges.map((edge) => ({
|
|
1589
|
-
id: edge.id,
|
|
1590
|
-
sourceId: edge.sourceId,
|
|
1591
|
-
targetId: edge.targetId,
|
|
1592
|
-
label: edge.label,
|
|
1593
|
-
data: edge.data,
|
|
1594
|
-
...edge.weight !== void 0 && { weight: edge.weight }
|
|
1595
|
-
}))
|
|
2321
|
+
nodes: graph.nodes.map((node) => toNodeConfig(node)),
|
|
2322
|
+
edges: mstEdges.map((edge) => toEdgeConfig(edge))
|
|
1596
2323
|
});
|
|
1597
2324
|
}
|
|
1598
2325
|
function primMST(graph, getWeight) {
|
|
@@ -1611,26 +2338,28 @@ function primMST(graph, getWeight) {
|
|
|
1611
2338
|
edge
|
|
1612
2339
|
});
|
|
1613
2340
|
}
|
|
1614
|
-
|
|
2341
|
+
for (const eid of idx.inEdges.get(nodeId) ?? []) {
|
|
1615
2342
|
const ai = idx.edgeById.get(eid);
|
|
1616
2343
|
if (ai === void 0) continue;
|
|
1617
2344
|
const edge = graph.edges[ai];
|
|
1618
|
-
if (!inMST.has(edge.sourceId)) candidates.push({
|
|
2345
|
+
if (getEdgeMode(graph, edge) !== "directed" && !inMST.has(edge.sourceId)) candidates.push({
|
|
1619
2346
|
weight: getWeight(edge),
|
|
1620
2347
|
edge
|
|
1621
2348
|
});
|
|
1622
2349
|
}
|
|
1623
2350
|
}
|
|
1624
|
-
const
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
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
|
+
}
|
|
1634
2363
|
}
|
|
1635
2364
|
return mstEdges;
|
|
1636
2365
|
}
|
|
@@ -1668,60 +2397,38 @@ function kruskalMST(graph, getWeight) {
|
|
|
1668
2397
|
function getNodeIds(graph) {
|
|
1669
2398
|
return graph.nodes.map((node) => node.id);
|
|
1670
2399
|
}
|
|
1671
|
-
function getNeighborIds(graph, nodeId) {
|
|
1672
|
-
const idx = getIndex(graph);
|
|
1673
|
-
const neighbors = [];
|
|
1674
|
-
for (const edgeId of idx.outEdges.get(nodeId) ?? []) {
|
|
1675
|
-
const edgeIndex = idx.edgeById.get(edgeId);
|
|
1676
|
-
if (edgeIndex !== void 0) neighbors.push(graph.edges[edgeIndex].targetId);
|
|
1677
|
-
}
|
|
1678
|
-
if (graph.type === "undirected") for (const edgeId of idx.inEdges.get(nodeId) ?? []) {
|
|
1679
|
-
const edgeIndex = idx.edgeById.get(edgeId);
|
|
1680
|
-
if (edgeIndex !== void 0) neighbors.push(graph.edges[edgeIndex].sourceId);
|
|
1681
|
-
}
|
|
1682
|
-
return neighbors;
|
|
1683
|
-
}
|
|
1684
|
-
function getIncomingIds(graph, nodeId) {
|
|
1685
|
-
const idx = getIndex(graph);
|
|
1686
|
-
const incoming = [];
|
|
1687
|
-
for (const edgeId of idx.inEdges.get(nodeId) ?? []) {
|
|
1688
|
-
const edgeIndex = idx.edgeById.get(edgeId);
|
|
1689
|
-
if (edgeIndex !== void 0) incoming.push(graph.edges[edgeIndex].sourceId);
|
|
1690
|
-
}
|
|
1691
|
-
if (graph.type === "undirected") for (const edgeId of idx.outEdges.get(nodeId) ?? []) {
|
|
1692
|
-
const edgeIndex = idx.edgeById.get(edgeId);
|
|
1693
|
-
if (edgeIndex !== void 0) incoming.push(graph.edges[edgeIndex].targetId);
|
|
1694
|
-
}
|
|
1695
|
-
return incoming;
|
|
1696
|
-
}
|
|
1697
2400
|
function createEmptyScoreMap(graph) {
|
|
1698
2401
|
return Object.fromEntries(graph.nodes.map((node) => [node.id, 0]));
|
|
1699
2402
|
}
|
|
1700
|
-
function
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
return
|
|
1705
|
-
|
|
1706
|
-
function maxDiff(previous, next) {
|
|
1707
|
-
let diff = 0;
|
|
1708
|
-
for (const key of Object.keys(next)) diff = Math.max(diff, Math.abs((previous[key] ?? 0) - next[key]));
|
|
1709
|
-
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;
|
|
1710
2409
|
}
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
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
|
+
}
|
|
1722
2429
|
}
|
|
1723
2430
|
}
|
|
1724
|
-
return
|
|
2431
|
+
return tail;
|
|
1725
2432
|
}
|
|
1726
2433
|
/**
|
|
1727
2434
|
* Returns degree centrality scores for all nodes.
|
|
@@ -1736,14 +2443,8 @@ function getReachableDistances(graph, startId) {
|
|
|
1736
2443
|
*/
|
|
1737
2444
|
function getDegreeCentrality(graph) {
|
|
1738
2445
|
const scale = graph.nodes.length > 1 ? 1 / (graph.nodes.length - 1) : 0;
|
|
1739
|
-
const idx = getIndex(graph);
|
|
1740
2446
|
const scores = createEmptyScoreMap(graph);
|
|
1741
|
-
for (const node of graph.nodes)
|
|
1742
|
-
const outDegree = idx.outEdges.get(node.id)?.length ?? 0;
|
|
1743
|
-
const inDegree = idx.inEdges.get(node.id)?.length ?? 0;
|
|
1744
|
-
const degree = graph.type === "undirected" ? new Set([...idx.outEdges.get(node.id) ?? [], ...idx.inEdges.get(node.id) ?? []]).size : outDegree + inDegree;
|
|
1745
|
-
scores[node.id] = degree * scale;
|
|
1746
|
-
}
|
|
2447
|
+
for (const node of graph.nodes) scores[node.id] = getDegree(graph, node.id) * scale;
|
|
1747
2448
|
return scores;
|
|
1748
2449
|
}
|
|
1749
2450
|
/**
|
|
@@ -1753,9 +2454,8 @@ function getDegreeCentrality(graph) {
|
|
|
1753
2454
|
*/
|
|
1754
2455
|
function getInDegreeCentrality(graph) {
|
|
1755
2456
|
const scale = graph.nodes.length > 1 ? 1 / (graph.nodes.length - 1) : 0;
|
|
1756
|
-
const idx = getIndex(graph);
|
|
1757
2457
|
const scores = createEmptyScoreMap(graph);
|
|
1758
|
-
for (const node of graph.nodes) scores[node.id] = (
|
|
2458
|
+
for (const node of graph.nodes) scores[node.id] = getInDegree(graph, node.id) * scale;
|
|
1759
2459
|
return scores;
|
|
1760
2460
|
}
|
|
1761
2461
|
/**
|
|
@@ -1765,9 +2465,8 @@ function getInDegreeCentrality(graph) {
|
|
|
1765
2465
|
*/
|
|
1766
2466
|
function getOutDegreeCentrality(graph) {
|
|
1767
2467
|
const scale = graph.nodes.length > 1 ? 1 / (graph.nodes.length - 1) : 0;
|
|
1768
|
-
const idx = getIndex(graph);
|
|
1769
2468
|
const scores = createEmptyScoreMap(graph);
|
|
1770
|
-
for (const node of graph.nodes) scores[node.id] = (
|
|
2469
|
+
for (const node of graph.nodes) scores[node.id] = getOutDegree(graph, node.id) * scale;
|
|
1771
2470
|
return scores;
|
|
1772
2471
|
}
|
|
1773
2472
|
/**
|
|
@@ -1778,16 +2477,19 @@ function getOutDegreeCentrality(graph) {
|
|
|
1778
2477
|
*/
|
|
1779
2478
|
function getClosenessCentrality(graph) {
|
|
1780
2479
|
const scores = createEmptyScoreMap(graph);
|
|
1781
|
-
const
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
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]];
|
|
1787
2490
|
if (totalDistance === 0) continue;
|
|
1788
|
-
const reachable = distances.size;
|
|
1789
2491
|
const closeness = reachable / totalDistance;
|
|
1790
|
-
scores[
|
|
2492
|
+
scores[csr.ids[s]] = order > 1 ? closeness * (reachable / (order - 1)) : closeness;
|
|
1791
2493
|
}
|
|
1792
2494
|
return scores;
|
|
1793
2495
|
}
|
|
@@ -1798,52 +2500,53 @@ function getClosenessCentrality(graph) {
|
|
|
1798
2500
|
* normalized scores.
|
|
1799
2501
|
*/
|
|
1800
2502
|
function getBetweennessCentrality(graph) {
|
|
1801
|
-
const
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
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;
|
|
1826
2527
|
}
|
|
2528
|
+
if (dist[v] === du + 1) sigma[v] += sigma[u];
|
|
1827
2529
|
}
|
|
1828
2530
|
}
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
const
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
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;
|
|
1838
2539
|
}
|
|
1839
|
-
if (
|
|
2540
|
+
if (w !== s) totals[w] += delta[w];
|
|
1840
2541
|
}
|
|
1841
2542
|
}
|
|
2543
|
+
const scores = createEmptyScoreMap(graph);
|
|
2544
|
+
for (let i = 0; i < n; i++) scores[csr.ids[i]] = totals[i];
|
|
1842
2545
|
const order = graph.nodes.length;
|
|
1843
2546
|
if (order <= 2) return scores;
|
|
1844
|
-
const scale = graph.
|
|
2547
|
+
const scale = graph.mode !== "directed" ? 1 / ((order - 1) * (order - 2) / 2) : 1 / ((order - 1) * (order - 2));
|
|
1845
2548
|
for (const nodeId of Object.keys(scores)) {
|
|
1846
|
-
if (graph.
|
|
2549
|
+
if (graph.mode !== "directed") scores[nodeId] /= 2;
|
|
1847
2550
|
scores[nodeId] *= scale;
|
|
1848
2551
|
}
|
|
1849
2552
|
return scores;
|
|
@@ -1860,28 +2563,32 @@ function getPageRank(graph, options) {
|
|
|
1860
2563
|
const maxIterations = options?.maxIterations ?? 100;
|
|
1861
2564
|
const tolerance = options?.tolerance ?? 1e-6;
|
|
1862
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]];
|
|
1863
2570
|
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
1864
|
-
const
|
|
2571
|
+
const next = new Float64Array(n).fill((1 - alpha) / n);
|
|
1865
2572
|
let danglingMass = 0;
|
|
1866
|
-
for (
|
|
1867
|
-
const
|
|
1868
|
-
if (
|
|
1869
|
-
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];
|
|
1870
2577
|
continue;
|
|
1871
2578
|
}
|
|
1872
|
-
const share =
|
|
1873
|
-
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;
|
|
1874
2581
|
}
|
|
1875
2582
|
if (danglingMass > 0) {
|
|
1876
|
-
const share = alpha * danglingMass /
|
|
1877
|
-
for (
|
|
1878
|
-
}
|
|
1879
|
-
if (maxDiff(scores, nextScores) <= tolerance) {
|
|
1880
|
-
scores = nextScores;
|
|
1881
|
-
break;
|
|
2583
|
+
const share = alpha * danglingMass / n;
|
|
2584
|
+
for (let i = 0; i < n; i++) next[i] += share;
|
|
1882
2585
|
}
|
|
1883
|
-
|
|
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;
|
|
1884
2590
|
}
|
|
2591
|
+
for (let i = 0; i < n; i++) scores[csr.ids[i]] = current[i];
|
|
1885
2592
|
const total = Object.values(scores).reduce((sum, value) => sum + value, 0);
|
|
1886
2593
|
if (total !== 0) for (const nodeId of nodeIds) scores[nodeId] /= total;
|
|
1887
2594
|
return scores;
|
|
@@ -1892,31 +2599,38 @@ function getPageRank(graph, options) {
|
|
|
1892
2599
|
* Uses power iteration and L2 normalization per iteration.
|
|
1893
2600
|
*/
|
|
1894
2601
|
function getHITS(graph, options) {
|
|
1895
|
-
|
|
1896
|
-
if (nodeIds.length === 0) return {
|
|
2602
|
+
if (getNodeIds(graph).length === 0) return {
|
|
1897
2603
|
hubs: {},
|
|
1898
2604
|
authorities: {}
|
|
1899
2605
|
};
|
|
1900
2606
|
const maxIterations = options?.maxIterations ?? 100;
|
|
1901
2607
|
const tolerance = options?.tolerance ?? 1e-6;
|
|
1902
|
-
|
|
1903
|
-
|
|
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);
|
|
1904
2612
|
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
1905
|
-
const nextAuthorities =
|
|
1906
|
-
for (
|
|
1907
|
-
|
|
1908
|
-
const nextHubs =
|
|
1909
|
-
for (
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
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]));
|
|
1913
2621
|
hubs = nextHubs;
|
|
1914
2622
|
authorities = nextAuthorities;
|
|
1915
|
-
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];
|
|
1916
2630
|
}
|
|
1917
2631
|
return {
|
|
1918
|
-
hubs,
|
|
1919
|
-
authorities
|
|
2632
|
+
hubs: hubScores,
|
|
2633
|
+
authorities: authorityScores
|
|
1920
2634
|
};
|
|
1921
2635
|
}
|
|
1922
2636
|
/**
|
|
@@ -1926,20 +2640,24 @@ function getHITS(graph, options) {
|
|
|
1926
2640
|
* undirected adjacency for undirected graphs.
|
|
1927
2641
|
*/
|
|
1928
2642
|
function getEigenvectorCentrality(graph, options) {
|
|
1929
|
-
|
|
1930
|
-
if (nodeIds.length === 0) return {};
|
|
2643
|
+
if (getNodeIds(graph).length === 0) return {};
|
|
1931
2644
|
const maxIterations = options?.maxIterations ?? 100;
|
|
1932
2645
|
const tolerance = options?.tolerance ?? 1e-6;
|
|
1933
|
-
|
|
1934
|
-
|
|
2646
|
+
const csr = getCSR(graph);
|
|
2647
|
+
const n = csr.ids.length;
|
|
2648
|
+
let current = new Float64Array(n).fill(1);
|
|
2649
|
+
normalizeTypedVector(current);
|
|
1935
2650
|
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
1936
|
-
const
|
|
1937
|
-
for (
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
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;
|
|
1941
2657
|
if (diff <= tolerance) break;
|
|
1942
2658
|
}
|
|
2659
|
+
const scores = createEmptyScoreMap(graph);
|
|
2660
|
+
for (let i = 0; i < n; i++) scores[csr.ids[i]] = current[i];
|
|
1943
2661
|
return scores;
|
|
1944
2662
|
}
|
|
1945
2663
|
|
|
@@ -2247,8 +2965,8 @@ function traverseConnectivity(graph, nodeId, parentEdgeId, state) {
|
|
|
2247
2965
|
traverseConnectivity(graph, neighbor.nodeId, neighbor.edgeId, state);
|
|
2248
2966
|
state.low.set(nodeId, Math.min(state.low.get(nodeId), state.low.get(neighbor.nodeId)));
|
|
2249
2967
|
if (state.low.get(neighbor.nodeId) > state.disc.get(nodeId)) state.bridges.add(neighbor.edgeId);
|
|
2250
|
-
if (
|
|
2251
|
-
state.articulationPoints.add(nodeId);
|
|
2968
|
+
if (state.low.get(neighbor.nodeId) >= state.disc.get(nodeId)) {
|
|
2969
|
+
if (parentEdgeId !== null) state.articulationPoints.add(nodeId);
|
|
2252
2970
|
popComponentUntil(state, neighbor.edgeId);
|
|
2253
2971
|
}
|
|
2254
2972
|
} else if (state.disc.get(neighbor.nodeId) < state.disc.get(nodeId)) {
|
|
@@ -2312,14 +3030,33 @@ function getBiconnectedComponents(graph) {
|
|
|
2312
3030
|
//#region src/algorithms/isomorphism.ts
|
|
2313
3031
|
function getDegreeSignature(graph, nodeId) {
|
|
2314
3032
|
const idx = getIndex(graph);
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
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}`;
|
|
2319
3054
|
}
|
|
2320
3055
|
function getEdgesBetween(graph, sourceId, targetId) {
|
|
2321
|
-
|
|
2322
|
-
|
|
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
|
+
});
|
|
2323
3060
|
}
|
|
2324
3061
|
function edgesAreCompatible(edgesA, edgesB, edgeMatch) {
|
|
2325
3062
|
if (edgesA.length !== edgesB.length) return false;
|
|
@@ -2339,7 +3076,6 @@ function edgesAreCompatible(edgesA, edgesB, edgeMatch) {
|
|
|
2339
3076
|
* node and edge payloads.
|
|
2340
3077
|
*/
|
|
2341
3078
|
function isIsomorphic(graphA, graphB, options) {
|
|
2342
|
-
if (graphA.type !== graphB.type) return false;
|
|
2343
3079
|
if (graphA.nodes.length !== graphB.nodes.length) return false;
|
|
2344
3080
|
if (graphA.edges.length !== graphB.edges.length) return false;
|
|
2345
3081
|
const nodeMatch = options?.nodeMatch;
|
|
@@ -2363,6 +3099,7 @@ function isIsomorphic(graphA, graphB, options) {
|
|
|
2363
3099
|
if (usedB.has(nodeB.id)) continue;
|
|
2364
3100
|
if (getDegreeSignature(graphB, nodeB.id) !== signatureA) continue;
|
|
2365
3101
|
if (nodeMatch && !nodeMatch(nodeA, nodeB)) continue;
|
|
3102
|
+
if (!edgesAreCompatible(getEdgesBetween(graphA, nodeA.id, nodeA.id), getEdgesBetween(graphB, nodeB.id, nodeB.id), edgeMatch)) continue;
|
|
2366
3103
|
let compatible = true;
|
|
2367
3104
|
for (const [mappedAId, mappedBId] of mapping.entries()) {
|
|
2368
3105
|
if (!edgesAreCompatible(getEdgesBetween(graphA, nodeA.id, mappedAId), getEdgesBetween(graphB, nodeB.id, mappedBId), edgeMatch)) {
|
|
@@ -2387,4 +3124,404 @@ function isIsomorphic(graphA, graphB, options) {
|
|
|
2387
3124
|
}
|
|
2388
3125
|
|
|
2389
3126
|
//#endregion
|
|
2390
|
-
|
|
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 };
|