@statelyai/graph 0.9.0 → 0.11.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/dist/{algorithms-3xvCxHzo.mjs → algorithms-BHHg7lGq.mjs} +1448 -1988
- package/dist/algorithms-BlM-qoJb.d.mts +178 -0
- package/dist/algorithms.d.mts +1 -1
- package/dist/algorithms.mjs +1 -1
- package/dist/{converter-B5CUD0r9.mjs → converter-Dspillnn.mjs} +2 -2
- package/dist/format-support.d.mts +22 -0
- package/dist/format-support.mjs +294 -0
- package/dist/formats/adjacency-list/index.d.mts +1 -1
- package/dist/formats/adjacency-list/index.mjs +1 -1
- package/dist/formats/converter/index.d.mts +1 -1
- package/dist/formats/converter/index.mjs +1 -1
- package/dist/formats/cytoscape/index.d.mts +1 -1
- package/dist/formats/cytoscape/index.mjs +1 -1
- package/dist/formats/d3/index.d.mts +1 -1
- package/dist/formats/d3/index.mjs +1 -1
- package/dist/formats/dot/index.d.mts +1 -1
- package/dist/formats/dot/index.mjs +1 -1
- 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/gexf/index.d.mts +1 -1
- package/dist/formats/gexf/index.mjs +1 -1
- package/dist/formats/gml/index.d.mts +1 -1
- package/dist/formats/gml/index.mjs +1 -1
- package/dist/formats/graphml/index.d.mts +1 -1
- package/dist/formats/graphml/index.mjs +1 -1
- package/dist/formats/jgf/index.d.mts +1 -1
- package/dist/formats/jgf/index.mjs +1 -1
- package/dist/formats/mermaid/index.d.mts +1 -1
- package/dist/formats/mermaid/index.mjs +1 -1
- package/dist/formats/tgf/index.d.mts +1 -1
- package/dist/formats/tgf/index.mjs +1 -1
- package/dist/formats/xyflow/index.d.mts +1 -1
- package/dist/index.d.mts +7 -7
- package/dist/index.mjs +5 -5
- package/dist/queries.d.mts +14 -14
- package/dist/queries.mjs +17 -10
- package/dist/schemas.d.mts +50 -1
- package/dist/schemas.mjs +21 -2
- package/dist/{types-BzckPChi.d.mts → types-CnZ01raw.d.mts} +31 -16
- package/package.json +5 -2
- package/schemas/edge.schema.json +16 -1
- package/schemas/graph.schema.json +72 -3
- package/schemas/node.schema.json +56 -1
- package/dist/algorithms-g2uWmPrb.d.mts +0 -786
- /package/dist/{adjacency-list-fldj-QAL.mjs → adjacency-list-Ca0VjKIf.mjs} +0 -0
- /package/dist/{edge-list-Br05wXMg.mjs → edge-list-gKe8-iRa.mjs} +0 -0
- /package/dist/{indexing-DyfgLuzw.mjs → indexing-CJc-ul8e.mjs} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as indexUpdateEdgeEndpoints, i as indexReparentNode, n as indexAddEdge, o as invalidateIndex, r as indexAddNode, t as getIndex } from "./indexing-
|
|
1
|
+
import { a as indexUpdateEdgeEndpoints, i as indexReparentNode, n as indexAddEdge, o as invalidateIndex, r as indexAddNode, t as getIndex } from "./indexing-CJc-ul8e.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/graph.ts
|
|
4
4
|
/**
|
|
@@ -49,7 +49,7 @@ function createGraphNode(config) {
|
|
|
49
49
|
id: config.id,
|
|
50
50
|
...config.parentId !== void 0 && { parentId: config.parentId ?? null },
|
|
51
51
|
...config.initialNodeId !== void 0 && { initialNodeId: config.initialNodeId ?? null },
|
|
52
|
-
label: config.label ??
|
|
52
|
+
label: config.label ?? null,
|
|
53
53
|
data: config.data
|
|
54
54
|
};
|
|
55
55
|
if (config.ports !== void 0 && config.ports.length > 0) {
|
|
@@ -650,893 +650,691 @@ function collectDescendants(graph, id) {
|
|
|
650
650
|
}
|
|
651
651
|
|
|
652
652
|
//#endregion
|
|
653
|
-
//#region src/algorithms/
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
const idx = getIndex(graph);
|
|
659
|
-
const neighbors = [];
|
|
660
|
-
for (const edgeId of idx.outEdges.get(nodeId) ?? []) {
|
|
661
|
-
const edgeIndex = idx.edgeById.get(edgeId);
|
|
662
|
-
if (edgeIndex !== void 0) neighbors.push(graph.edges[edgeIndex].targetId);
|
|
653
|
+
//#region src/algorithms/shared.ts
|
|
654
|
+
var MinPriorityQueue = class {
|
|
655
|
+
items = [];
|
|
656
|
+
constructor(compare) {
|
|
657
|
+
this.compare = compare;
|
|
663
658
|
}
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
if (edgeIndex !== void 0) neighbors.push(graph.edges[edgeIndex].sourceId);
|
|
659
|
+
get size() {
|
|
660
|
+
return this.items.length;
|
|
667
661
|
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
const idx = getIndex(graph);
|
|
672
|
-
const incoming = [];
|
|
673
|
-
for (const edgeId of idx.inEdges.get(nodeId) ?? []) {
|
|
674
|
-
const edgeIndex = idx.edgeById.get(edgeId);
|
|
675
|
-
if (edgeIndex !== void 0) incoming.push(graph.edges[edgeIndex].sourceId);
|
|
662
|
+
push(item) {
|
|
663
|
+
this.items.push(item);
|
|
664
|
+
this.bubbleUp(this.items.length - 1);
|
|
676
665
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
666
|
+
pop() {
|
|
667
|
+
if (this.items.length === 0) return void 0;
|
|
668
|
+
const first = this.items[0];
|
|
669
|
+
const last = this.items.pop();
|
|
670
|
+
if (this.items.length > 0) {
|
|
671
|
+
this.items[0] = last;
|
|
672
|
+
this.bubbleDown(0);
|
|
673
|
+
}
|
|
674
|
+
return first;
|
|
680
675
|
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
if (magnitude === 0) return scores;
|
|
689
|
-
for (const key of Object.keys(scores)) scores[key] /= magnitude;
|
|
690
|
-
return scores;
|
|
691
|
-
}
|
|
692
|
-
function maxDiff(previous, next) {
|
|
693
|
-
let diff = 0;
|
|
694
|
-
for (const key of Object.keys(next)) diff = Math.max(diff, Math.abs((previous[key] ?? 0) - next[key]));
|
|
695
|
-
return diff;
|
|
696
|
-
}
|
|
697
|
-
function getReachableDistances(graph, startId) {
|
|
698
|
-
const distances = /* @__PURE__ */ new Map();
|
|
699
|
-
const queue = [startId];
|
|
700
|
-
distances.set(startId, 0);
|
|
701
|
-
while (queue.length > 0) {
|
|
702
|
-
const currentId = queue.shift();
|
|
703
|
-
const currentDistance = distances.get(currentId);
|
|
704
|
-
for (const neighborId of getNeighborIds$1(graph, currentId)) {
|
|
705
|
-
if (distances.has(neighborId)) continue;
|
|
706
|
-
distances.set(neighborId, currentDistance + 1);
|
|
707
|
-
queue.push(neighborId);
|
|
676
|
+
bubbleUp(index) {
|
|
677
|
+
let current = index;
|
|
678
|
+
while (current > 0) {
|
|
679
|
+
const parent = Math.floor((current - 1) / 2);
|
|
680
|
+
if (this.compare(this.items[current], this.items[parent]) >= 0) break;
|
|
681
|
+
[this.items[current], this.items[parent]] = [this.items[parent], this.items[current]];
|
|
682
|
+
current = parent;
|
|
708
683
|
}
|
|
709
684
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
685
|
+
bubbleDown(index) {
|
|
686
|
+
let current = index;
|
|
687
|
+
while (true) {
|
|
688
|
+
const left = current * 2 + 1;
|
|
689
|
+
const right = left + 1;
|
|
690
|
+
let smallest = current;
|
|
691
|
+
if (left < this.items.length && this.compare(this.items[left], this.items[smallest]) < 0) smallest = left;
|
|
692
|
+
if (right < this.items.length && this.compare(this.items[right], this.items[smallest]) < 0) smallest = right;
|
|
693
|
+
if (smallest === current) break;
|
|
694
|
+
[this.items[current], this.items[smallest]] = [this.items[smallest], this.items[current]];
|
|
695
|
+
current = smallest;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
function getNeighborIds$1(graph, nodeId) {
|
|
725
700
|
const idx = getIndex(graph);
|
|
726
|
-
const
|
|
727
|
-
for (const
|
|
728
|
-
const
|
|
729
|
-
|
|
730
|
-
const degree = graph.type === "undirected" ? new Set([...idx.outEdges.get(node.id) ?? [], ...idx.inEdges.get(node.id) ?? []]).size : outDegree + inDegree;
|
|
731
|
-
scores[node.id] = degree * scale;
|
|
701
|
+
const ids = [];
|
|
702
|
+
for (const eid of idx.outEdges.get(nodeId) ?? []) {
|
|
703
|
+
const ai = idx.edgeById.get(eid);
|
|
704
|
+
if (ai !== void 0) ids.push(graph.edges[ai].targetId);
|
|
732
705
|
}
|
|
733
|
-
|
|
706
|
+
if (graph.type === "undirected") for (const eid of idx.inEdges.get(nodeId) ?? []) {
|
|
707
|
+
const ai = idx.edgeById.get(eid);
|
|
708
|
+
if (ai !== void 0) ids.push(graph.edges[ai].sourceId);
|
|
709
|
+
}
|
|
710
|
+
return ids;
|
|
734
711
|
}
|
|
735
|
-
|
|
736
|
-
* Returns in-degree centrality scores for all nodes.
|
|
737
|
-
*
|
|
738
|
-
* In-degree centrality is the incoming degree normalized by `n - 1`.
|
|
739
|
-
*/
|
|
740
|
-
function getInDegreeCentrality(graph) {
|
|
741
|
-
const scale = graph.nodes.length > 1 ? 1 / (graph.nodes.length - 1) : 0;
|
|
712
|
+
function getSuccessorIds(graph, nodeId) {
|
|
742
713
|
const idx = getIndex(graph);
|
|
743
|
-
|
|
744
|
-
for (const node of graph.nodes) scores[node.id] = (idx.inEdges.get(node.id)?.length ?? 0) * scale;
|
|
745
|
-
return scores;
|
|
714
|
+
return (idx.outEdges.get(nodeId) ?? []).map((eid) => graph.edges[idx.edgeById.get(eid)].targetId);
|
|
746
715
|
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
const
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
for (const node of graph.nodes) scores[node.id] = (idx.outEdges.get(node.id)?.length ?? 0) * scale;
|
|
757
|
-
return scores;
|
|
716
|
+
function resolveFrom(graph, opts) {
|
|
717
|
+
if (opts?.from) return opts.from;
|
|
718
|
+
if (graph.initialNodeId) return graph.initialNodeId;
|
|
719
|
+
const inDeg = /* @__PURE__ */ new Map();
|
|
720
|
+
for (const node of graph.nodes) inDeg.set(node.id, 0);
|
|
721
|
+
for (const edge of graph.edges) inDeg.set(edge.targetId, (inDeg.get(edge.targetId) ?? 0) + 1);
|
|
722
|
+
const roots = [...inDeg.entries()].filter(([, degree]) => degree === 0).map(([id]) => id);
|
|
723
|
+
if (roots.length === 1) return roots[0];
|
|
724
|
+
throw new Error("Cannot determine start node — provide opts.from or set graph.initialNodeId");
|
|
758
725
|
}
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
if (distances.size === 0) continue;
|
|
772
|
-
const totalDistance = [...distances.values()].reduce((sum, distance) => sum + distance, 0);
|
|
773
|
-
if (totalDistance === 0) continue;
|
|
774
|
-
const reachable = distances.size;
|
|
775
|
-
const closeness = reachable / totalDistance;
|
|
776
|
-
scores[node.id] = order > 1 ? closeness * (reachable / (order - 1)) : closeness;
|
|
726
|
+
function getNeighborEdges(graph, nodeId) {
|
|
727
|
+
const idx = getIndex(graph);
|
|
728
|
+
const result = [];
|
|
729
|
+
for (const eid of idx.outEdges.get(nodeId) ?? []) {
|
|
730
|
+
const ai = idx.edgeById.get(eid);
|
|
731
|
+
if (ai !== void 0) {
|
|
732
|
+
const edge = graph.edges[ai];
|
|
733
|
+
result.push({
|
|
734
|
+
neighborId: edge.targetId,
|
|
735
|
+
edge
|
|
736
|
+
});
|
|
737
|
+
}
|
|
777
738
|
}
|
|
778
|
-
|
|
739
|
+
if (graph.type === "undirected") for (const eid of idx.inEdges.get(nodeId) ?? []) {
|
|
740
|
+
const ai = idx.edgeById.get(eid);
|
|
741
|
+
if (ai !== void 0) {
|
|
742
|
+
const edge = graph.edges[ai];
|
|
743
|
+
result.push({
|
|
744
|
+
neighborId: edge.sourceId,
|
|
745
|
+
edge
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return result;
|
|
779
750
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
const sigma = /* @__PURE__ */ new Map();
|
|
792
|
-
const distance = /* @__PURE__ */ new Map();
|
|
793
|
-
const queue = [source.id];
|
|
794
|
-
for (const node of graph.nodes) {
|
|
795
|
-
predecessors.set(node.id, []);
|
|
796
|
-
sigma.set(node.id, 0);
|
|
797
|
-
distance.set(node.id, -1);
|
|
751
|
+
function getNeighborEdgesAll(graph, nodeId) {
|
|
752
|
+
const idx = getIndex(graph);
|
|
753
|
+
const result = [];
|
|
754
|
+
for (const eid of idx.outEdges.get(nodeId) ?? []) {
|
|
755
|
+
const ai = idx.edgeById.get(eid);
|
|
756
|
+
if (ai !== void 0) {
|
|
757
|
+
const edge = graph.edges[ai];
|
|
758
|
+
result.push({
|
|
759
|
+
neighborId: edge.targetId,
|
|
760
|
+
edge
|
|
761
|
+
});
|
|
798
762
|
}
|
|
799
|
-
|
|
800
|
-
|
|
763
|
+
}
|
|
764
|
+
for (const eid of idx.inEdges.get(nodeId) ?? []) {
|
|
765
|
+
const ai = idx.edgeById.get(eid);
|
|
766
|
+
if (ai !== void 0) {
|
|
767
|
+
const edge = graph.edges[ai];
|
|
768
|
+
result.push({
|
|
769
|
+
neighborId: edge.sourceId,
|
|
770
|
+
edge
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
return result;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
//#endregion
|
|
778
|
+
//#region src/algorithms/paths.ts
|
|
779
|
+
function computeShortestDistances(graph, sourceId, getWeight, algorithm) {
|
|
780
|
+
if (algorithm === "bellman-ford") return bellmanFord(graph, sourceId, getWeight);
|
|
781
|
+
const dist = /* @__PURE__ */ new Map();
|
|
782
|
+
const prev = /* @__PURE__ */ new Map();
|
|
783
|
+
dist.set(sourceId, 0);
|
|
784
|
+
prev.set(sourceId, []);
|
|
785
|
+
if (!getWeight && !graph.edges.some((edge) => edge.weight !== void 0)) {
|
|
786
|
+
const queue = [sourceId];
|
|
801
787
|
while (queue.length > 0) {
|
|
802
|
-
const
|
|
803
|
-
|
|
804
|
-
for (const neighborId of
|
|
805
|
-
|
|
788
|
+
const id = queue.shift();
|
|
789
|
+
const distance = dist.get(id);
|
|
790
|
+
for (const { neighborId, edge } of getNeighborEdges(graph, id)) {
|
|
791
|
+
const nextDistance = distance + 1;
|
|
792
|
+
const existing = dist.get(neighborId);
|
|
793
|
+
if (existing === void 0) {
|
|
794
|
+
dist.set(neighborId, nextDistance);
|
|
795
|
+
prev.set(neighborId, [{
|
|
796
|
+
from: id,
|
|
797
|
+
edge
|
|
798
|
+
}]);
|
|
806
799
|
queue.push(neighborId);
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
predecessors.get(neighborId).push(currentId);
|
|
812
|
-
}
|
|
800
|
+
} else if (existing === nextDistance) prev.get(neighborId).push({
|
|
801
|
+
from: id,
|
|
802
|
+
edge
|
|
803
|
+
});
|
|
813
804
|
}
|
|
814
805
|
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
806
|
+
} else {
|
|
807
|
+
const effectiveWeight = getWeight ?? ((edge) => edge.weight ?? 1);
|
|
808
|
+
const visited = /* @__PURE__ */ new Set();
|
|
809
|
+
const pq = new MinPriorityQueue((a, b) => a.dist - b.dist);
|
|
810
|
+
pq.push({
|
|
811
|
+
id: sourceId,
|
|
812
|
+
dist: 0
|
|
813
|
+
});
|
|
814
|
+
while (pq.size > 0) {
|
|
815
|
+
const { id, dist: distance } = pq.pop();
|
|
816
|
+
if (visited.has(id) || distance !== dist.get(id)) continue;
|
|
817
|
+
visited.add(id);
|
|
818
|
+
for (const { neighborId, edge } of getNeighborEdges(graph, id)) {
|
|
819
|
+
const nextDistance = distance + effectiveWeight(edge);
|
|
820
|
+
const existing = dist.get(neighborId);
|
|
821
|
+
if (existing === void 0 || nextDistance < existing) {
|
|
822
|
+
dist.set(neighborId, nextDistance);
|
|
823
|
+
prev.set(neighborId, [{
|
|
824
|
+
from: id,
|
|
825
|
+
edge
|
|
826
|
+
}]);
|
|
827
|
+
pq.push({
|
|
828
|
+
id: neighborId,
|
|
829
|
+
dist: nextDistance
|
|
830
|
+
});
|
|
831
|
+
} else if (existing === nextDistance) prev.get(neighborId).push({
|
|
832
|
+
from: id,
|
|
833
|
+
edge
|
|
834
|
+
});
|
|
824
835
|
}
|
|
825
|
-
if (nodeId !== source.id) scores[nodeId] += delta.get(nodeId);
|
|
826
836
|
}
|
|
827
837
|
}
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
if (graph.type === "undirected") scores[nodeId] /= 2;
|
|
833
|
-
scores[nodeId] *= scale;
|
|
834
|
-
}
|
|
835
|
-
return scores;
|
|
838
|
+
return {
|
|
839
|
+
dist,
|
|
840
|
+
prev
|
|
841
|
+
};
|
|
836
842
|
}
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
const
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
843
|
+
function bellmanFord(graph, sourceId, getWeight) {
|
|
844
|
+
const dist = /* @__PURE__ */ new Map();
|
|
845
|
+
const prev = /* @__PURE__ */ new Map();
|
|
846
|
+
const effectiveWeight = getWeight ?? ((edge) => edge.weight ?? 1);
|
|
847
|
+
const isUndirected = graph.type === "undirected";
|
|
848
|
+
for (const node of graph.nodes) {
|
|
849
|
+
dist.set(node.id, Infinity);
|
|
850
|
+
prev.set(node.id, []);
|
|
851
|
+
}
|
|
852
|
+
dist.set(sourceId, 0);
|
|
853
|
+
const directedEdges = [];
|
|
854
|
+
for (const edge of graph.edges) {
|
|
855
|
+
directedEdges.push({
|
|
856
|
+
fromId: edge.sourceId,
|
|
857
|
+
toId: edge.targetId,
|
|
858
|
+
edge
|
|
859
|
+
});
|
|
860
|
+
if (isUndirected) directedEdges.push({
|
|
861
|
+
fromId: edge.targetId,
|
|
862
|
+
toId: edge.sourceId,
|
|
863
|
+
edge
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
for (let i = 0; i < graph.nodes.length - 1; i++) {
|
|
867
|
+
let changed = false;
|
|
868
|
+
for (const { fromId, toId, edge } of directedEdges) {
|
|
869
|
+
const distance = dist.get(fromId);
|
|
870
|
+
if (distance === Infinity) continue;
|
|
871
|
+
const nextDistance = distance + effectiveWeight(edge);
|
|
872
|
+
const existing = dist.get(toId);
|
|
873
|
+
if (nextDistance < existing) {
|
|
874
|
+
dist.set(toId, nextDistance);
|
|
875
|
+
prev.set(toId, [{
|
|
876
|
+
from: fromId,
|
|
877
|
+
edge
|
|
878
|
+
}]);
|
|
879
|
+
changed = true;
|
|
880
|
+
} else if (nextDistance === existing && existing !== Infinity) {
|
|
881
|
+
const predecessors = prev.get(toId);
|
|
882
|
+
if (!predecessors.some((entry) => entry.from === fromId && entry.edge === edge)) predecessors.push({
|
|
883
|
+
from: fromId,
|
|
884
|
+
edge
|
|
885
|
+
});
|
|
857
886
|
}
|
|
858
|
-
const share = scores[nodeId] / neighbors.length;
|
|
859
|
-
for (const neighborId of neighbors) nextScores[neighborId] += alpha * share;
|
|
860
|
-
}
|
|
861
|
-
if (danglingMass > 0) {
|
|
862
|
-
const share = alpha * danglingMass / nodeIds.length;
|
|
863
|
-
for (const nodeId of nodeIds) nextScores[nodeId] += share;
|
|
864
|
-
}
|
|
865
|
-
if (maxDiff(scores, nextScores) <= tolerance) {
|
|
866
|
-
scores = nextScores;
|
|
867
|
-
break;
|
|
868
887
|
}
|
|
869
|
-
|
|
888
|
+
if (!changed) break;
|
|
870
889
|
}
|
|
871
|
-
const
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
*/
|
|
880
|
-
function getHITS(graph, options) {
|
|
881
|
-
const nodeIds = getNodeIds(graph);
|
|
882
|
-
if (nodeIds.length === 0) return {
|
|
883
|
-
hubs: {},
|
|
884
|
-
authorities: {}
|
|
885
|
-
};
|
|
886
|
-
const maxIterations = options?.maxIterations ?? 100;
|
|
887
|
-
const tolerance = options?.tolerance ?? 1e-6;
|
|
888
|
-
let hubs = Object.fromEntries(nodeIds.map((nodeId) => [nodeId, 1]));
|
|
889
|
-
let authorities = createEmptyScoreMap(graph);
|
|
890
|
-
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
891
|
-
const nextAuthorities = createEmptyScoreMap(graph);
|
|
892
|
-
for (const nodeId of nodeIds) for (const predecessorId of getIncomingIds(graph, nodeId)) nextAuthorities[nodeId] += hubs[predecessorId];
|
|
893
|
-
normalizeVector(nextAuthorities);
|
|
894
|
-
const nextHubs = createEmptyScoreMap(graph);
|
|
895
|
-
for (const nodeId of nodeIds) for (const neighborId of getNeighborIds$1(graph, nodeId)) nextHubs[nodeId] += nextAuthorities[neighborId];
|
|
896
|
-
normalizeVector(nextHubs);
|
|
897
|
-
const hubDiff = maxDiff(hubs, nextHubs);
|
|
898
|
-
const authorityDiff = maxDiff(authorities, nextAuthorities);
|
|
899
|
-
hubs = nextHubs;
|
|
900
|
-
authorities = nextAuthorities;
|
|
901
|
-
if (Math.max(hubDiff, authorityDiff) <= tolerance) break;
|
|
890
|
+
for (const { fromId, toId, edge } of directedEdges) {
|
|
891
|
+
const distance = dist.get(fromId);
|
|
892
|
+
if (distance === Infinity) continue;
|
|
893
|
+
if (distance + effectiveWeight(edge) < dist.get(toId)) throw new Error("Graph contains a negative-weight cycle reachable from the source node");
|
|
894
|
+
}
|
|
895
|
+
for (const [id, distance] of dist) if (distance === Infinity) {
|
|
896
|
+
dist.delete(id);
|
|
897
|
+
prev.delete(id);
|
|
902
898
|
}
|
|
903
899
|
return {
|
|
904
|
-
|
|
905
|
-
|
|
900
|
+
dist,
|
|
901
|
+
prev
|
|
906
902
|
};
|
|
907
903
|
}
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
const nodeIds = getNodeIds(graph);
|
|
916
|
-
if (nodeIds.length === 0) return {};
|
|
917
|
-
const maxIterations = options?.maxIterations ?? 100;
|
|
918
|
-
const tolerance = options?.tolerance ?? 1e-6;
|
|
919
|
-
let scores = Object.fromEntries(nodeIds.map((nodeId) => [nodeId, 1]));
|
|
920
|
-
normalizeVector(scores);
|
|
921
|
-
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
922
|
-
const nextScores = createEmptyScoreMap(graph);
|
|
923
|
-
for (const nodeId of nodeIds) for (const predecessorId of getIncomingIds(graph, nodeId)) nextScores[nodeId] += scores[predecessorId];
|
|
924
|
-
normalizeVector(nextScores);
|
|
925
|
-
const diff = maxDiff(scores, nextScores);
|
|
926
|
-
scores = nextScores;
|
|
927
|
-
if (diff <= tolerance) break;
|
|
904
|
+
function* reconstructPaths(graph, prev, sourceNode, targetId) {
|
|
905
|
+
if (targetId === sourceNode.id) {
|
|
906
|
+
yield {
|
|
907
|
+
source: sourceNode,
|
|
908
|
+
steps: []
|
|
909
|
+
};
|
|
910
|
+
return;
|
|
928
911
|
}
|
|
929
|
-
|
|
912
|
+
const predecessors = prev.get(targetId);
|
|
913
|
+
if (!predecessors || predecessors.length === 0) return;
|
|
914
|
+
const targetNi = getIndex(graph).nodeById.get(targetId);
|
|
915
|
+
const targetNode = targetNi !== void 0 ? graph.nodes[targetNi] : graph.nodes.find((node) => node.id === targetId);
|
|
916
|
+
for (const { from, edge } of predecessors) for (const prefix of reconstructPaths(graph, prev, sourceNode, from)) yield {
|
|
917
|
+
source: sourceNode,
|
|
918
|
+
steps: [...prefix.steps, {
|
|
919
|
+
edge,
|
|
920
|
+
node: targetNode
|
|
921
|
+
}]
|
|
922
|
+
};
|
|
930
923
|
}
|
|
931
|
-
|
|
932
|
-
//#endregion
|
|
933
|
-
//#region src/algorithms/community.ts
|
|
934
|
-
function getUndirectedNeighbors$1(graph, nodeId) {
|
|
924
|
+
function* genShortestPaths(graph, opts) {
|
|
935
925
|
const idx = getIndex(graph);
|
|
936
|
-
const
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
});
|
|
943
|
-
}
|
|
944
|
-
for (const edgeId of idx.inEdges.get(nodeId) ?? []) {
|
|
945
|
-
const edgeIndex = idx.edgeById.get(edgeId);
|
|
946
|
-
if (edgeIndex !== void 0) neighbors.push({
|
|
947
|
-
nodeId: graph.edges[edgeIndex].sourceId,
|
|
948
|
-
edgeId
|
|
949
|
-
});
|
|
950
|
-
}
|
|
951
|
-
return neighbors;
|
|
926
|
+
const sourceId = resolveFrom(graph, opts);
|
|
927
|
+
const { dist, prev } = computeShortestDistances(graph, sourceId, opts?.getWeight, opts?.algorithm);
|
|
928
|
+
const targets = opts?.to ? [opts.to].filter((id) => dist.has(id)) : [...dist.keys()].filter((id) => id !== sourceId);
|
|
929
|
+
const sourceNi = idx.nodeById.get(sourceId);
|
|
930
|
+
const sourceNode = sourceNi !== void 0 ? graph.nodes[sourceNi] : graph.nodes.find((node) => node.id === sourceId);
|
|
931
|
+
for (const targetId of targets) yield* reconstructPaths(graph, prev, sourceNode, targetId);
|
|
952
932
|
}
|
|
953
|
-
function
|
|
933
|
+
function getShortestPaths(graph, opts) {
|
|
934
|
+
return [...genShortestPaths(graph, opts)];
|
|
935
|
+
}
|
|
936
|
+
function getShortestPath(graph, opts) {
|
|
937
|
+
for (const path of genShortestPaths(graph, opts)) return path;
|
|
938
|
+
}
|
|
939
|
+
function getSimplePaths(graph, opts) {
|
|
940
|
+
return [...genSimplePaths(graph, opts)];
|
|
941
|
+
}
|
|
942
|
+
function* genSimplePaths(graph, opts) {
|
|
954
943
|
const idx = getIndex(graph);
|
|
944
|
+
const sourceId = resolveFrom(graph, opts);
|
|
945
|
+
const sourceNi = idx.nodeById.get(sourceId);
|
|
946
|
+
const sourceNode = sourceNi !== void 0 ? graph.nodes[sourceNi] : graph.nodes.find((node) => node.id === sourceId);
|
|
947
|
+
const targetId = opts?.to;
|
|
955
948
|
const visited = /* @__PURE__ */ new Set();
|
|
956
|
-
const
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
if (visited.has(neighbor.nodeId)) continue;
|
|
968
|
-
visited.add(neighbor.nodeId);
|
|
969
|
-
queue.push(neighbor.nodeId);
|
|
949
|
+
const currentSteps = [];
|
|
950
|
+
function* dfsCollect(nodeId) {
|
|
951
|
+
visited.add(nodeId);
|
|
952
|
+
if (targetId !== void 0) {
|
|
953
|
+
if (nodeId === targetId) {
|
|
954
|
+
yield {
|
|
955
|
+
source: sourceNode,
|
|
956
|
+
steps: [...currentSteps]
|
|
957
|
+
};
|
|
958
|
+
visited.delete(nodeId);
|
|
959
|
+
return;
|
|
970
960
|
}
|
|
961
|
+
} else if (currentSteps.length > 0) yield {
|
|
962
|
+
source: sourceNode,
|
|
963
|
+
steps: [...currentSteps]
|
|
964
|
+
};
|
|
965
|
+
for (const { neighborId, edge } of getNeighborEdges(graph, nodeId)) if (!visited.has(neighborId)) {
|
|
966
|
+
const neighborNi = idx.nodeById.get(neighborId);
|
|
967
|
+
const neighborNode = neighborNi !== void 0 ? graph.nodes[neighborNi] : graph.nodes.find((node) => node.id === neighborId);
|
|
968
|
+
currentSteps.push({
|
|
969
|
+
edge,
|
|
970
|
+
node: neighborNode
|
|
971
|
+
});
|
|
972
|
+
yield* dfsCollect(neighborId);
|
|
973
|
+
currentSteps.pop();
|
|
971
974
|
}
|
|
972
|
-
|
|
975
|
+
visited.delete(nodeId);
|
|
973
976
|
}
|
|
974
|
-
|
|
975
|
-
}
|
|
976
|
-
function getNodeMap(graph) {
|
|
977
|
-
return new Map(graph.nodes.map((node) => [node.id, node]));
|
|
977
|
+
yield* dfsCollect(sourceId);
|
|
978
978
|
}
|
|
979
|
-
function
|
|
980
|
-
const
|
|
981
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
982
|
-
for (const [nodeId, label] of Object.entries(labels)) {
|
|
983
|
-
if (!grouped.has(label)) grouped.set(label, []);
|
|
984
|
-
const node = nodeMap.get(nodeId);
|
|
985
|
-
if (node) grouped.get(label).push(node);
|
|
986
|
-
}
|
|
987
|
-
return [...grouped.values()].map((community) => community.sort((a, b) => a.id.localeCompare(b.id))).sort((a, b) => a[0].id.localeCompare(b[0].id));
|
|
979
|
+
function getSimplePath(graph, opts) {
|
|
980
|
+
for (const path of genSimplePaths(graph, opts)) return path;
|
|
988
981
|
}
|
|
989
|
-
function
|
|
990
|
-
const
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
}
|
|
1012
|
-
if (distance.get(neighbor.nodeId) === distance.get(currentId) + 1) {
|
|
1013
|
-
sigma.set(neighbor.nodeId, sigma.get(neighbor.nodeId) + sigma.get(currentId));
|
|
1014
|
-
predecessors.get(neighbor.nodeId).push({
|
|
1015
|
-
nodeId: currentId,
|
|
1016
|
-
edgeId: neighbor.edgeId
|
|
1017
|
-
});
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
982
|
+
function getStronglyConnectedComponents(graph) {
|
|
983
|
+
const idx = getIndex(graph);
|
|
984
|
+
let indexCounter = 0;
|
|
985
|
+
const nodeIndex = /* @__PURE__ */ new Map();
|
|
986
|
+
const lowlink = /* @__PURE__ */ new Map();
|
|
987
|
+
const onStack = /* @__PURE__ */ new Set();
|
|
988
|
+
const stack = [];
|
|
989
|
+
const result = [];
|
|
990
|
+
function strongconnect(id) {
|
|
991
|
+
nodeIndex.set(id, indexCounter);
|
|
992
|
+
lowlink.set(id, indexCounter);
|
|
993
|
+
indexCounter++;
|
|
994
|
+
stack.push(id);
|
|
995
|
+
onStack.add(id);
|
|
996
|
+
for (const eid of idx.outEdges.get(id) ?? []) {
|
|
997
|
+
const ai = idx.edgeById.get(eid);
|
|
998
|
+
if (ai === void 0) continue;
|
|
999
|
+
const neighborId = graph.edges[ai].targetId;
|
|
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)));
|
|
1020
1004
|
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
}
|
|
1005
|
+
if (lowlink.get(id) === nodeIndex.get(id)) {
|
|
1006
|
+
const component = [];
|
|
1007
|
+
let neighborId;
|
|
1008
|
+
do {
|
|
1009
|
+
neighborId = stack.pop();
|
|
1010
|
+
onStack.delete(neighborId);
|
|
1011
|
+
const ni = idx.nodeById.get(neighborId);
|
|
1012
|
+
if (ni !== void 0) component.push(graph.nodes[ni]);
|
|
1013
|
+
} while (neighborId !== id);
|
|
1014
|
+
result.push(component);
|
|
1032
1015
|
}
|
|
1033
1016
|
}
|
|
1034
|
-
for (const
|
|
1035
|
-
return
|
|
1017
|
+
for (const node of graph.nodes) if (!nodeIndex.has(node.id)) strongconnect(node.id);
|
|
1018
|
+
return result;
|
|
1036
1019
|
}
|
|
1037
|
-
function
|
|
1038
|
-
return
|
|
1039
|
-
...graph,
|
|
1040
|
-
nodes: [...graph.nodes],
|
|
1041
|
-
edges
|
|
1042
|
-
};
|
|
1020
|
+
function getCycles(graph) {
|
|
1021
|
+
return [...genCycles(graph)];
|
|
1043
1022
|
}
|
|
1044
|
-
function
|
|
1045
|
-
|
|
1023
|
+
function* genCycles(graph) {
|
|
1024
|
+
if (graph.type === "undirected") yield* genCyclesUndirected(graph);
|
|
1025
|
+
else yield* genCyclesDirected(graph);
|
|
1046
1026
|
}
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
const
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1027
|
+
function* genCyclesDirected(graph) {
|
|
1028
|
+
const idx = getIndex(graph);
|
|
1029
|
+
const sortedIds = graph.nodes.map((node) => node.id).sort();
|
|
1030
|
+
for (let startIndex = 0; startIndex < sortedIds.length; startIndex++) {
|
|
1031
|
+
const startId = sortedIds[startIndex];
|
|
1032
|
+
const allowed = new Set(sortedIds.slice(startIndex));
|
|
1033
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1034
|
+
const steps = [];
|
|
1035
|
+
const startNi = idx.nodeById.get(startId);
|
|
1036
|
+
const startNode = graph.nodes[startNi];
|
|
1037
|
+
const found = [];
|
|
1038
|
+
function dfsFind(currentId) {
|
|
1039
|
+
visited.add(currentId);
|
|
1040
|
+
for (const eid of idx.outEdges.get(currentId) ?? []) {
|
|
1041
|
+
const ai = idx.edgeById.get(eid);
|
|
1042
|
+
if (ai === void 0) continue;
|
|
1043
|
+
const edge = graph.edges[ai];
|
|
1044
|
+
const neighborId = edge.targetId;
|
|
1045
|
+
if (neighborId === startId && (steps.length > 0 || currentId === startId)) found.push({
|
|
1046
|
+
source: startNode,
|
|
1047
|
+
steps: [...steps, {
|
|
1048
|
+
edge,
|
|
1049
|
+
node: startNode
|
|
1050
|
+
}]
|
|
1051
|
+
});
|
|
1052
|
+
else if (allowed.has(neighborId) && !visited.has(neighborId)) {
|
|
1053
|
+
const ni = idx.nodeById.get(neighborId);
|
|
1054
|
+
steps.push({
|
|
1055
|
+
edge,
|
|
1056
|
+
node: graph.nodes[ni]
|
|
1057
|
+
});
|
|
1058
|
+
dfsFind(neighborId);
|
|
1059
|
+
steps.pop();
|
|
1060
|
+
}
|
|
1075
1061
|
}
|
|
1062
|
+
visited.delete(currentId);
|
|
1076
1063
|
}
|
|
1077
|
-
|
|
1078
|
-
|
|
1064
|
+
dfsFind(startId);
|
|
1065
|
+
yield* found;
|
|
1079
1066
|
}
|
|
1080
|
-
return normalizeCommunities(graph, labels);
|
|
1081
1067
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
*/
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
const
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1068
|
+
function* genCyclesUndirected(graph) {
|
|
1069
|
+
const idx = getIndex(graph);
|
|
1070
|
+
const sortedIds = graph.nodes.map((node) => node.id).sort();
|
|
1071
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1072
|
+
for (let startIndex = 0; startIndex < sortedIds.length; startIndex++) {
|
|
1073
|
+
const startId = sortedIds[startIndex];
|
|
1074
|
+
const allowed = new Set(sortedIds.slice(startIndex));
|
|
1075
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1076
|
+
const steps = [];
|
|
1077
|
+
const startNi = idx.nodeById.get(startId);
|
|
1078
|
+
const startNode = graph.nodes[startNi];
|
|
1079
|
+
const found = [];
|
|
1080
|
+
function dfsFind(currentId, parentId) {
|
|
1081
|
+
visited.add(currentId);
|
|
1082
|
+
for (const { neighborId, edge } of getNeighborEdgesAll(graph, currentId)) {
|
|
1083
|
+
if (neighborId === parentId) {
|
|
1084
|
+
parentId = null;
|
|
1085
|
+
continue;
|
|
1086
|
+
}
|
|
1087
|
+
if (neighborId === startId && steps.length >= 2) {
|
|
1088
|
+
const innerIds = steps.map((step) => step.node.id).sort().join(",");
|
|
1089
|
+
if (!seen.has(innerIds)) {
|
|
1090
|
+
seen.add(innerIds);
|
|
1091
|
+
found.push({
|
|
1092
|
+
source: startNode,
|
|
1093
|
+
steps: [...steps, {
|
|
1094
|
+
edge,
|
|
1095
|
+
node: startNode
|
|
1096
|
+
}]
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
} else if (allowed.has(neighborId) && !visited.has(neighborId)) {
|
|
1100
|
+
const ni = idx.nodeById.get(neighborId);
|
|
1101
|
+
steps.push({
|
|
1102
|
+
edge,
|
|
1103
|
+
node: graph.nodes[ni]
|
|
1104
|
+
});
|
|
1105
|
+
dfsFind(neighborId, currentId);
|
|
1106
|
+
steps.pop();
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
visited.delete(currentId);
|
|
1101
1110
|
}
|
|
1111
|
+
dfsFind(startId, null);
|
|
1112
|
+
yield* found;
|
|
1102
1113
|
}
|
|
1103
1114
|
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
function getGirvanNewmanCommunities(graph, options) {
|
|
1110
|
-
if (graph.nodes.length === 0) return [];
|
|
1111
|
-
const targetLevel = options?.level ?? 1;
|
|
1112
|
-
if (targetLevel <= 0) return getUndirectedConnectedComponents(graph);
|
|
1113
|
-
let last = getUndirectedConnectedComponents(graph);
|
|
1114
|
-
let level = 0;
|
|
1115
|
-
for (const partition of genGirvanNewmanCommunities(graph, { maxLevels: targetLevel })) {
|
|
1116
|
-
last = partition;
|
|
1117
|
-
level++;
|
|
1118
|
-
if (level >= targetLevel) break;
|
|
1119
|
-
}
|
|
1120
|
-
return last;
|
|
1115
|
+
function getAllPairsShortestPaths(graph, opts) {
|
|
1116
|
+
const algorithm = opts?.algorithm ?? "dijkstra";
|
|
1117
|
+
if (algorithm === "floyd-warshall") return floydWarshallAllPaths(graph, opts?.getWeight);
|
|
1118
|
+
if (algorithm === "bellman-ford") return bellmanFordAllPaths(graph, opts?.getWeight);
|
|
1119
|
+
return dijkstraAllPaths(graph, opts?.getWeight);
|
|
1121
1120
|
}
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1121
|
+
function bellmanFordAllPaths(graph, getWeight) {
|
|
1122
|
+
const results = [];
|
|
1123
|
+
for (const node of graph.nodes) results.push(...getShortestPaths(graph, {
|
|
1124
|
+
from: node.id,
|
|
1125
|
+
getWeight,
|
|
1126
|
+
algorithm: "bellman-ford"
|
|
1127
|
+
}));
|
|
1128
|
+
return results;
|
|
1129
|
+
}
|
|
1130
|
+
function dijkstraAllPaths(graph, getWeight) {
|
|
1131
|
+
const results = [];
|
|
1132
|
+
for (const node of graph.nodes) results.push(...getShortestPaths(graph, {
|
|
1133
|
+
from: node.id,
|
|
1134
|
+
getWeight
|
|
1135
|
+
}));
|
|
1136
|
+
return results;
|
|
1137
|
+
}
|
|
1138
|
+
function floydWarshallAllPaths(graph, getWeight) {
|
|
1139
|
+
const idx = getIndex(graph);
|
|
1140
|
+
const weight = getWeight ?? ((edge) => edge.weight ?? 1);
|
|
1129
1141
|
const nodeIds = graph.nodes.map((node) => node.id);
|
|
1130
|
-
const
|
|
1131
|
-
const
|
|
1132
|
-
for (
|
|
1142
|
+
const nodeCount = nodeIds.length;
|
|
1143
|
+
const indexOf = /* @__PURE__ */ new Map();
|
|
1144
|
+
for (let i = 0; i < nodeCount; i++) indexOf.set(nodeIds[i], i);
|
|
1145
|
+
const INF = Infinity;
|
|
1146
|
+
const dist = Array.from({ length: nodeCount }, () => Array(nodeCount).fill(INF));
|
|
1147
|
+
const prev = Array.from({ length: nodeCount }, () => Array.from({ length: nodeCount }, () => []));
|
|
1148
|
+
for (let i = 0; i < nodeCount; i++) dist[i][i] = 0;
|
|
1133
1149
|
for (const edge of graph.edges) {
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1150
|
+
const source = indexOf.get(edge.sourceId);
|
|
1151
|
+
const target = indexOf.get(edge.targetId);
|
|
1152
|
+
const edgeWeight = weight(edge);
|
|
1153
|
+
if (edgeWeight < dist[source][target]) {
|
|
1154
|
+
dist[source][target] = edgeWeight;
|
|
1155
|
+
prev[source][target] = [{
|
|
1156
|
+
from: source,
|
|
1157
|
+
edge
|
|
1158
|
+
}];
|
|
1159
|
+
} else if (edgeWeight === dist[source][target] && edgeWeight < INF) prev[source][target].push({
|
|
1160
|
+
from: source,
|
|
1161
|
+
edge
|
|
1162
|
+
});
|
|
1163
|
+
if (graph.type === "undirected") {
|
|
1164
|
+
if (edgeWeight < dist[target][source]) {
|
|
1165
|
+
dist[target][source] = edgeWeight;
|
|
1166
|
+
prev[target][source] = [{
|
|
1167
|
+
from: target,
|
|
1168
|
+
edge
|
|
1169
|
+
}];
|
|
1170
|
+
} else if (edgeWeight === dist[target][source] && edgeWeight < INF) prev[target][source].push({
|
|
1171
|
+
from: target,
|
|
1172
|
+
edge
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1138
1175
|
}
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1176
|
+
for (let k = 0; k < nodeCount; k++) for (let i = 0; i < nodeCount; i++) for (let j = 0; j < nodeCount; j++) {
|
|
1177
|
+
if (dist[i][k] === INF || dist[k][j] === INF) continue;
|
|
1178
|
+
const nextDistance = dist[i][k] + dist[k][j];
|
|
1179
|
+
if (nextDistance < dist[i][j]) {
|
|
1180
|
+
dist[i][j] = nextDistance;
|
|
1181
|
+
prev[i][j] = prev[k][j].map((entry) => ({ ...entry }));
|
|
1182
|
+
} else if (nextDistance === dist[i][j] && nextDistance < INF) {
|
|
1183
|
+
for (const entry of prev[k][j]) if (!prev[i][j].some((existing) => existing.edge.id === entry.edge.id)) prev[i][j].push({ ...entry });
|
|
1146
1184
|
}
|
|
1147
1185
|
}
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
let communities = graph.nodes.map((node) => [node]);
|
|
1157
|
-
let currentScore = getModularity(graph, communities);
|
|
1158
|
-
while (communities.length > 1) {
|
|
1159
|
-
let bestScore = currentScore;
|
|
1160
|
-
let bestMerge;
|
|
1161
|
-
for (let i = 0; i < communities.length; i++) for (let j = i + 1; j < communities.length; j++) {
|
|
1162
|
-
const merged = communities.filter((_, index) => index !== i && index !== j);
|
|
1163
|
-
merged.push([...communities[i], ...communities[j]].sort((a, b) => a.id.localeCompare(b.id)));
|
|
1164
|
-
const score = getModularity(graph, merged);
|
|
1165
|
-
if (score > bestScore + 1e-12) {
|
|
1166
|
-
bestScore = score;
|
|
1167
|
-
bestMerge = merged;
|
|
1168
|
-
}
|
|
1186
|
+
const results = [];
|
|
1187
|
+
for (let i = 0; i < nodeCount; i++) {
|
|
1188
|
+
const sourceNi = idx.nodeById.get(nodeIds[i]);
|
|
1189
|
+
if (sourceNi === void 0) continue;
|
|
1190
|
+
const sourceNode = graph.nodes[sourceNi];
|
|
1191
|
+
for (let j = 0; j < nodeCount; j++) {
|
|
1192
|
+
if (i === j || dist[i][j] === INF) continue;
|
|
1193
|
+
results.push(...fwReconstruct(graph, prev, nodeIds, sourceNode, i, j));
|
|
1169
1194
|
}
|
|
1170
|
-
if (!bestMerge) break;
|
|
1171
|
-
communities = bestMerge.sort((a, b) => a[0].id.localeCompare(b[0].id));
|
|
1172
|
-
currentScore = bestScore;
|
|
1173
1195
|
}
|
|
1174
|
-
return
|
|
1196
|
+
return results;
|
|
1175
1197
|
}
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
const
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1198
|
+
function fwReconstruct(graph, prev, nodeIds, sourceNode, sourceIdx, targetIdx) {
|
|
1199
|
+
if (sourceIdx === targetIdx) return [{
|
|
1200
|
+
source: sourceNode,
|
|
1201
|
+
steps: []
|
|
1202
|
+
}];
|
|
1203
|
+
const predecessors = prev[sourceIdx][targetIdx];
|
|
1204
|
+
if (predecessors.length === 0) return [];
|
|
1205
|
+
const targetNi = getIndex(graph).nodeById.get(nodeIds[targetIdx]);
|
|
1206
|
+
if (targetNi === void 0) return [];
|
|
1207
|
+
const targetNode = graph.nodes[targetNi];
|
|
1208
|
+
const results = [];
|
|
1209
|
+
for (const { from, edge } of predecessors) {
|
|
1210
|
+
const prefixPaths = fwReconstruct(graph, prev, nodeIds, sourceNode, sourceIdx, from);
|
|
1211
|
+
for (const prefix of prefixPaths) results.push({
|
|
1212
|
+
source: sourceNode,
|
|
1213
|
+
steps: [...prefix.steps, {
|
|
1214
|
+
edge,
|
|
1215
|
+
node: targetNode
|
|
1216
|
+
}]
|
|
1194
1217
|
});
|
|
1195
1218
|
}
|
|
1196
|
-
return
|
|
1197
|
-
}
|
|
1198
|
-
function popComponentUntil(state, stopEdgeId) {
|
|
1199
|
-
const nodeIds = /* @__PURE__ */ new Set();
|
|
1200
|
-
while (state.edgeStack.length > 0) {
|
|
1201
|
-
const edgeId = state.edgeStack.pop();
|
|
1202
|
-
const edge = state.edgeById.get(edgeId);
|
|
1203
|
-
if (edge) {
|
|
1204
|
-
nodeIds.add(edge.sourceId);
|
|
1205
|
-
nodeIds.add(edge.targetId);
|
|
1206
|
-
}
|
|
1207
|
-
if (edgeId === stopEdgeId) break;
|
|
1208
|
-
}
|
|
1209
|
-
if (nodeIds.size > 0) state.components.push(nodeIds);
|
|
1219
|
+
return results;
|
|
1210
1220
|
}
|
|
1211
|
-
function
|
|
1212
|
-
|
|
1213
|
-
const
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1221
|
+
function getAStarPath(graph, opts) {
|
|
1222
|
+
const idx = getIndex(graph);
|
|
1223
|
+
const { from: sourceId, to: targetId, heuristic } = opts;
|
|
1224
|
+
const getWeight = opts.getWeight ?? ((edge) => edge.weight ?? 1);
|
|
1225
|
+
const sourceNi = idx.nodeById.get(sourceId);
|
|
1226
|
+
if (sourceNi === void 0) return void 0;
|
|
1227
|
+
if (!idx.nodeById.has(targetId)) return void 0;
|
|
1228
|
+
if (sourceId === targetId) return {
|
|
1229
|
+
source: graph.nodes[sourceNi],
|
|
1230
|
+
steps: []
|
|
1231
|
+
};
|
|
1232
|
+
const gScore = /* @__PURE__ */ new Map();
|
|
1233
|
+
const cameFrom = /* @__PURE__ */ new Map();
|
|
1234
|
+
const closedSet = /* @__PURE__ */ new Set();
|
|
1235
|
+
const openSet = new MinPriorityQueue((a, b) => a.f - b.f);
|
|
1236
|
+
gScore.set(sourceId, 0);
|
|
1237
|
+
openSet.push({
|
|
1238
|
+
id: sourceId,
|
|
1239
|
+
f: heuristic(sourceId)
|
|
1240
|
+
});
|
|
1241
|
+
while (openSet.size > 0) {
|
|
1242
|
+
const { id: currentId } = openSet.pop();
|
|
1243
|
+
if (closedSet.has(currentId)) continue;
|
|
1244
|
+
if (currentId === targetId) {
|
|
1245
|
+
const steps = [];
|
|
1246
|
+
let current = targetId;
|
|
1247
|
+
while (current !== sourceId) {
|
|
1248
|
+
const previous = cameFrom.get(current);
|
|
1249
|
+
const ni = idx.nodeById.get(current);
|
|
1250
|
+
steps.unshift({
|
|
1251
|
+
edge: previous.edge,
|
|
1252
|
+
node: graph.nodes[ni]
|
|
1253
|
+
});
|
|
1254
|
+
current = previous.from;
|
|
1255
|
+
}
|
|
1256
|
+
return {
|
|
1257
|
+
source: graph.nodes[sourceNi],
|
|
1258
|
+
steps
|
|
1259
|
+
};
|
|
1219
1260
|
}
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
state.low.set(nodeId, Math.min(state.low.get(nodeId), state.low.get(neighbor.nodeId)));
|
|
1235
|
-
if (state.low.get(neighbor.nodeId) > state.disc.get(nodeId)) state.bridges.add(neighbor.edgeId);
|
|
1236
|
-
if (parentEdgeId !== null && state.low.get(neighbor.nodeId) >= state.disc.get(nodeId)) {
|
|
1237
|
-
state.articulationPoints.add(nodeId);
|
|
1238
|
-
popComponentUntil(state, neighbor.edgeId);
|
|
1261
|
+
closedSet.add(currentId);
|
|
1262
|
+
for (const { neighborId, edge } of getNeighborEdges(graph, currentId)) {
|
|
1263
|
+
if (closedSet.has(neighborId)) continue;
|
|
1264
|
+
const tentativeScore = (gScore.get(currentId) ?? Infinity) + getWeight(edge);
|
|
1265
|
+
if (tentativeScore < (gScore.get(neighborId) ?? Infinity)) {
|
|
1266
|
+
cameFrom.set(neighborId, {
|
|
1267
|
+
from: currentId,
|
|
1268
|
+
edge
|
|
1269
|
+
});
|
|
1270
|
+
gScore.set(neighborId, tentativeScore);
|
|
1271
|
+
openSet.push({
|
|
1272
|
+
id: neighborId,
|
|
1273
|
+
f: tentativeScore + heuristic(neighborId)
|
|
1274
|
+
});
|
|
1239
1275
|
}
|
|
1240
|
-
} else if (state.disc.get(neighbor.nodeId) < state.disc.get(nodeId)) {
|
|
1241
|
-
state.edgeStack.push(neighbor.edgeId);
|
|
1242
|
-
state.low.set(nodeId, Math.min(state.low.get(nodeId), state.disc.get(neighbor.nodeId)));
|
|
1243
1276
|
}
|
|
1244
1277
|
}
|
|
1245
|
-
if (parentEdgeId === null && childCount > 1) state.articulationPoints.add(nodeId);
|
|
1246
1278
|
}
|
|
1247
|
-
function
|
|
1248
|
-
const
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
bridges: /* @__PURE__ */ new Set(),
|
|
1254
|
-
articulationPoints: /* @__PURE__ */ new Set(),
|
|
1255
|
-
components: [],
|
|
1256
|
-
nodeById: new Map(graph.nodes.map((node) => [node.id, node])),
|
|
1257
|
-
edgeById: new Map(graph.edges.map((edge) => [edge.id, edge]))
|
|
1279
|
+
function joinPaths(headPath, tailPath) {
|
|
1280
|
+
const headEnd = headPath.steps.length > 0 ? headPath.steps[headPath.steps.length - 1].node : headPath.source;
|
|
1281
|
+
if (headEnd.id !== tailPath.source.id) throw new Error(`Paths cannot be joined: head path ends at "${headEnd.id}" but tail path starts at "${tailPath.source.id}"`);
|
|
1282
|
+
return {
|
|
1283
|
+
source: headPath.source,
|
|
1284
|
+
steps: [...headPath.steps, ...tailPath.steps]
|
|
1258
1285
|
};
|
|
1259
|
-
for (const node of graph.nodes) {
|
|
1260
|
-
if (state.disc.has(node.id)) continue;
|
|
1261
|
-
traverseConnectivity(graph, node.id, null, state);
|
|
1262
|
-
finalizeRemainingComponent(state);
|
|
1263
|
-
}
|
|
1264
|
-
return state;
|
|
1265
|
-
}
|
|
1266
|
-
/**
|
|
1267
|
-
* Returns bridge edges whose removal disconnects the graph.
|
|
1268
|
-
*
|
|
1269
|
-
* Connectivity algorithms in this module treat the graph as undirected.
|
|
1270
|
-
*/
|
|
1271
|
-
function getBridges(graph) {
|
|
1272
|
-
if (graph.edges.length === 0) return [];
|
|
1273
|
-
const state = analyzeConnectivity(graph);
|
|
1274
|
-
return [...state.bridges].map((edgeId) => state.edgeById.get(edgeId)).sort((a, b) => a.id.localeCompare(b.id));
|
|
1275
|
-
}
|
|
1276
|
-
/**
|
|
1277
|
-
* Returns articulation points (cut vertices) for the graph.
|
|
1278
|
-
*
|
|
1279
|
-
* Connectivity algorithms in this module treat the graph as undirected.
|
|
1280
|
-
*/
|
|
1281
|
-
function getArticulationPoints(graph) {
|
|
1282
|
-
if (graph.nodes.length === 0) return [];
|
|
1283
|
-
const state = analyzeConnectivity(graph);
|
|
1284
|
-
return [...state.articulationPoints].map((nodeId) => state.nodeById.get(nodeId)).sort((a, b) => a.id.localeCompare(b.id));
|
|
1285
|
-
}
|
|
1286
|
-
/**
|
|
1287
|
-
* Returns biconnected components as arrays of nodes.
|
|
1288
|
-
*
|
|
1289
|
-
* Articulation points may appear in multiple returned components.
|
|
1290
|
-
*/
|
|
1291
|
-
function getBiconnectedComponents(graph) {
|
|
1292
|
-
if (graph.edges.length === 0) return [];
|
|
1293
|
-
const state = analyzeConnectivity(graph);
|
|
1294
|
-
return state.components.map((component) => [...component].map((nodeId) => state.nodeById.get(nodeId)).sort((a, b) => a.id.localeCompare(b.id))).sort((a, b) => a[0].id.localeCompare(b[0].id));
|
|
1295
1286
|
}
|
|
1296
1287
|
|
|
1297
1288
|
//#endregion
|
|
1298
|
-
//#region src/algorithms/
|
|
1299
|
-
function
|
|
1289
|
+
//#region src/algorithms/traversal.ts
|
|
1290
|
+
function* bfs(graph, startId) {
|
|
1300
1291
|
const idx = getIndex(graph);
|
|
1301
|
-
const
|
|
1302
|
-
const
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1292
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1293
|
+
const queue = [startId];
|
|
1294
|
+
visited.add(startId);
|
|
1295
|
+
while (queue.length > 0) {
|
|
1296
|
+
const id = queue.shift();
|
|
1297
|
+
const ni = idx.nodeById.get(id);
|
|
1298
|
+
if (ni === void 0) continue;
|
|
1299
|
+
yield graph.nodes[ni];
|
|
1300
|
+
for (const neighborId of getNeighborIds$1(graph, id)) if (!visited.has(neighborId)) {
|
|
1301
|
+
visited.add(neighborId);
|
|
1302
|
+
queue.push(neighborId);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1309
1305
|
}
|
|
1310
|
-
function
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
const
|
|
1314
|
-
|
|
1315
|
-
const
|
|
1316
|
-
if (
|
|
1317
|
-
|
|
1306
|
+
function* dfs(graph, startId) {
|
|
1307
|
+
const idx = getIndex(graph);
|
|
1308
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1309
|
+
const stack = [startId];
|
|
1310
|
+
while (stack.length > 0) {
|
|
1311
|
+
const id = stack.pop();
|
|
1312
|
+
if (visited.has(id)) continue;
|
|
1313
|
+
visited.add(id);
|
|
1314
|
+
const ni = idx.nodeById.get(id);
|
|
1315
|
+
if (ni === void 0) continue;
|
|
1316
|
+
yield graph.nodes[ni];
|
|
1317
|
+
for (const neighborId of getNeighborIds$1(graph, id)) if (!visited.has(neighborId)) stack.push(neighborId);
|
|
1318
1318
|
}
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
const nodesB = [...graphB.nodes];
|
|
1339
|
-
const signaturesA = nodesA.map((node) => getDegreeSignature(graphA, node.id)).sort();
|
|
1340
|
-
const signaturesB = nodesB.map((node) => getDegreeSignature(graphB, node.id)).sort();
|
|
1341
|
-
if (signaturesA.join("|") !== signaturesB.join("|")) return false;
|
|
1342
|
-
const mapping = /* @__PURE__ */ new Map();
|
|
1343
|
-
const usedB = /* @__PURE__ */ new Set();
|
|
1344
|
-
const backtrack = (index) => {
|
|
1345
|
-
if (index >= nodesA.length) return true;
|
|
1346
|
-
const nodeA = nodesA[index];
|
|
1347
|
-
const signatureA = getDegreeSignature(graphA, nodeA.id);
|
|
1348
|
-
for (const nodeB of nodesB) {
|
|
1349
|
-
if (usedB.has(nodeB.id)) continue;
|
|
1350
|
-
if (getDegreeSignature(graphB, nodeB.id) !== signatureA) continue;
|
|
1351
|
-
if (nodeMatch && !nodeMatch(nodeA, nodeB)) continue;
|
|
1352
|
-
let compatible = true;
|
|
1353
|
-
for (const [mappedAId, mappedBId] of mapping.entries()) {
|
|
1354
|
-
if (!edgesAreCompatible(getEdgesBetween(graphA, nodeA.id, mappedAId), getEdgesBetween(graphB, nodeB.id, mappedBId), edgeMatch)) {
|
|
1355
|
-
compatible = false;
|
|
1356
|
-
break;
|
|
1357
|
-
}
|
|
1358
|
-
if (!edgesAreCompatible(getEdgesBetween(graphA, mappedAId, nodeA.id), getEdgesBetween(graphB, mappedBId, nodeB.id), edgeMatch)) {
|
|
1359
|
-
compatible = false;
|
|
1360
|
-
break;
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
|
-
if (!compatible) continue;
|
|
1364
|
-
mapping.set(nodeA.id, nodeB.id);
|
|
1365
|
-
usedB.add(nodeB.id);
|
|
1366
|
-
if (backtrack(index + 1)) return true;
|
|
1367
|
-
mapping.delete(nodeA.id);
|
|
1368
|
-
usedB.delete(nodeB.id);
|
|
1369
|
-
}
|
|
1370
|
-
return false;
|
|
1371
|
-
};
|
|
1372
|
-
return backtrack(0);
|
|
1373
|
-
}
|
|
1374
|
-
|
|
1375
|
-
//#endregion
|
|
1376
|
-
//#region src/algorithms.ts
|
|
1377
|
-
/**
|
|
1378
|
-
* Breadth-first traversal generator yielding nodes level by level.
|
|
1379
|
-
*
|
|
1380
|
-
* **O(V + E)** time, **O(V)** space.
|
|
1381
|
-
*
|
|
1382
|
-
* @example
|
|
1383
|
-
* ```ts
|
|
1384
|
-
* import { createGraph, bfs } from '@statelyai/graph';
|
|
1385
|
-
*
|
|
1386
|
-
* const graph = createGraph({
|
|
1387
|
-
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1388
|
-
* edges: [{ id: 'ab', sourceId: 'a', targetId: 'b' }, { id: 'bc', sourceId: 'b', targetId: 'c' }],
|
|
1389
|
-
* });
|
|
1390
|
-
*
|
|
1391
|
-
* for (const node of bfs(graph, 'a')) {
|
|
1392
|
-
* console.log(node.id); // 'a', 'b', 'c'
|
|
1393
|
-
* }
|
|
1394
|
-
* ```
|
|
1395
|
-
*/
|
|
1396
|
-
function* bfs(graph, startId) {
|
|
1397
|
-
const idx = getIndex(graph);
|
|
1398
|
-
const visited = /* @__PURE__ */ new Set();
|
|
1399
|
-
const queue = [startId];
|
|
1400
|
-
visited.add(startId);
|
|
1401
|
-
while (queue.length > 0) {
|
|
1402
|
-
const id = queue.shift();
|
|
1403
|
-
const ni = idx.nodeById.get(id);
|
|
1404
|
-
if (ni === void 0) continue;
|
|
1405
|
-
yield graph.nodes[ni];
|
|
1406
|
-
for (const nId of getNeighborIds(graph, id)) if (!visited.has(nId)) {
|
|
1407
|
-
visited.add(nId);
|
|
1408
|
-
queue.push(nId);
|
|
1409
|
-
}
|
|
1410
|
-
}
|
|
1411
|
-
}
|
|
1412
|
-
/**
|
|
1413
|
-
* Depth-first traversal generator yielding nodes as visited.
|
|
1414
|
-
*
|
|
1415
|
-
* **O(V + E)** time, **O(V)** space.
|
|
1416
|
-
*
|
|
1417
|
-
* @example
|
|
1418
|
-
* ```ts
|
|
1419
|
-
* import { createGraph, dfs } from '@statelyai/graph';
|
|
1420
|
-
*
|
|
1421
|
-
* const graph = createGraph({
|
|
1422
|
-
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1423
|
-
* edges: [{ id: 'ab', sourceId: 'a', targetId: 'b' }, { id: 'bc', sourceId: 'b', targetId: 'c' }],
|
|
1424
|
-
* });
|
|
1425
|
-
*
|
|
1426
|
-
* for (const node of dfs(graph, 'a')) {
|
|
1427
|
-
* console.log(node.id); // 'a', 'b', 'c'
|
|
1428
|
-
* }
|
|
1429
|
-
* ```
|
|
1430
|
-
*/
|
|
1431
|
-
function* dfs(graph, startId) {
|
|
1432
|
-
const idx = getIndex(graph);
|
|
1433
|
-
const visited = /* @__PURE__ */ new Set();
|
|
1434
|
-
const stack = [startId];
|
|
1435
|
-
while (stack.length > 0) {
|
|
1436
|
-
const id = stack.pop();
|
|
1437
|
-
if (visited.has(id)) continue;
|
|
1438
|
-
visited.add(id);
|
|
1439
|
-
const ni = idx.nodeById.get(id);
|
|
1440
|
-
if (ni === void 0) continue;
|
|
1441
|
-
yield graph.nodes[ni];
|
|
1442
|
-
for (const nId of getNeighborIds(graph, id)) if (!visited.has(nId)) stack.push(nId);
|
|
1443
|
-
}
|
|
1444
|
-
}
|
|
1445
|
-
function getNeighborIds(graph, nodeId) {
|
|
1446
|
-
const idx = getIndex(graph);
|
|
1447
|
-
const ids = [];
|
|
1448
|
-
for (const eid of idx.outEdges.get(nodeId) ?? []) {
|
|
1449
|
-
const ai = idx.edgeById.get(eid);
|
|
1450
|
-
if (ai !== void 0) ids.push(graph.edges[ai].targetId);
|
|
1451
|
-
}
|
|
1452
|
-
if (graph.type === "undirected") for (const eid of idx.inEdges.get(nodeId) ?? []) {
|
|
1453
|
-
const ai = idx.edgeById.get(eid);
|
|
1454
|
-
if (ai !== void 0) ids.push(graph.edges[ai].sourceId);
|
|
1455
|
-
}
|
|
1456
|
-
return ids;
|
|
1457
|
-
}
|
|
1458
|
-
function getSuccessorIds(graph, nodeId) {
|
|
1459
|
-
const idx = getIndex(graph);
|
|
1460
|
-
return (idx.outEdges.get(nodeId) ?? []).map((eid) => graph.edges[idx.edgeById.get(eid)].targetId);
|
|
1461
|
-
}
|
|
1462
|
-
var MinPriorityQueue = class {
|
|
1463
|
-
items = [];
|
|
1464
|
-
constructor(compare) {
|
|
1465
|
-
this.compare = compare;
|
|
1466
|
-
}
|
|
1467
|
-
get size() {
|
|
1468
|
-
return this.items.length;
|
|
1469
|
-
}
|
|
1470
|
-
push(item) {
|
|
1471
|
-
this.items.push(item);
|
|
1472
|
-
this.bubbleUp(this.items.length - 1);
|
|
1473
|
-
}
|
|
1474
|
-
pop() {
|
|
1475
|
-
if (this.items.length === 0) return void 0;
|
|
1476
|
-
const first = this.items[0];
|
|
1477
|
-
const last = this.items.pop();
|
|
1478
|
-
if (this.items.length > 0) {
|
|
1479
|
-
this.items[0] = last;
|
|
1480
|
-
this.bubbleDown(0);
|
|
1481
|
-
}
|
|
1482
|
-
return first;
|
|
1483
|
-
}
|
|
1484
|
-
bubbleUp(index) {
|
|
1485
|
-
let current = index;
|
|
1486
|
-
while (current > 0) {
|
|
1487
|
-
const parent = Math.floor((current - 1) / 2);
|
|
1488
|
-
if (this.compare(this.items[current], this.items[parent]) >= 0) break;
|
|
1489
|
-
[this.items[current], this.items[parent]] = [this.items[parent], this.items[current]];
|
|
1490
|
-
current = parent;
|
|
1491
|
-
}
|
|
1492
|
-
}
|
|
1493
|
-
bubbleDown(index) {
|
|
1494
|
-
let current = index;
|
|
1495
|
-
while (true) {
|
|
1496
|
-
const left = current * 2 + 1;
|
|
1497
|
-
const right = left + 1;
|
|
1498
|
-
let smallest = current;
|
|
1499
|
-
if (left < this.items.length && this.compare(this.items[left], this.items[smallest]) < 0) smallest = left;
|
|
1500
|
-
if (right < this.items.length && this.compare(this.items[right], this.items[smallest]) < 0) smallest = right;
|
|
1501
|
-
if (smallest === current) break;
|
|
1502
|
-
[this.items[current], this.items[smallest]] = [this.items[smallest], this.items[current]];
|
|
1503
|
-
current = smallest;
|
|
1504
|
-
}
|
|
1505
|
-
}
|
|
1506
|
-
};
|
|
1507
|
-
/**
|
|
1508
|
-
* Checks whether the graph contains no cycles.
|
|
1509
|
-
*
|
|
1510
|
-
* **O(V + E)** time.
|
|
1511
|
-
*
|
|
1512
|
-
* @example
|
|
1513
|
-
* ```ts
|
|
1514
|
-
* import { createGraph, isAcyclic } from '@statelyai/graph';
|
|
1515
|
-
*
|
|
1516
|
-
* const dag = createGraph({
|
|
1517
|
-
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
1518
|
-
* edges: [{ id: 'ab', sourceId: 'a', targetId: 'b' }],
|
|
1519
|
-
* });
|
|
1520
|
-
*
|
|
1521
|
-
* isAcyclic(dag); // true
|
|
1522
|
-
* ```
|
|
1523
|
-
*/
|
|
1524
|
-
function isAcyclic(graph) {
|
|
1525
|
-
if (graph.type === "undirected") return isAcyclicUndirected(graph);
|
|
1526
|
-
const WHITE = 0, GRAY = 1, BLACK = 2;
|
|
1527
|
-
const color = /* @__PURE__ */ new Map();
|
|
1528
|
-
for (const n of graph.nodes) color.set(n.id, WHITE);
|
|
1529
|
-
const hasCycle = (id) => {
|
|
1530
|
-
color.set(id, GRAY);
|
|
1531
|
-
for (const nId of getSuccessorIds(graph, id)) {
|
|
1532
|
-
const c = color.get(nId);
|
|
1533
|
-
if (c === GRAY) return true;
|
|
1534
|
-
if (c === WHITE && hasCycle(nId)) return true;
|
|
1535
|
-
}
|
|
1536
|
-
color.set(id, BLACK);
|
|
1537
|
-
return false;
|
|
1538
|
-
};
|
|
1539
|
-
for (const n of graph.nodes) if (color.get(n.id) === WHITE && hasCycle(n.id)) return false;
|
|
1319
|
+
}
|
|
1320
|
+
function isAcyclic(graph) {
|
|
1321
|
+
if (graph.type === "undirected") return isAcyclicUndirected(graph);
|
|
1322
|
+
const WHITE = 0;
|
|
1323
|
+
const GRAY = 1;
|
|
1324
|
+
const BLACK = 2;
|
|
1325
|
+
const color = /* @__PURE__ */ new Map();
|
|
1326
|
+
for (const node of graph.nodes) color.set(node.id, WHITE);
|
|
1327
|
+
const hasCycle = (id) => {
|
|
1328
|
+
color.set(id, GRAY);
|
|
1329
|
+
for (const neighborId of getSuccessorIds(graph, id)) {
|
|
1330
|
+
const current = color.get(neighborId);
|
|
1331
|
+
if (current === GRAY) return true;
|
|
1332
|
+
if (current === WHITE && hasCycle(neighborId)) return true;
|
|
1333
|
+
}
|
|
1334
|
+
color.set(id, BLACK);
|
|
1335
|
+
return false;
|
|
1336
|
+
};
|
|
1337
|
+
for (const node of graph.nodes) if (color.get(node.id) === WHITE && hasCycle(node.id)) return false;
|
|
1540
1338
|
return true;
|
|
1541
1339
|
}
|
|
1542
1340
|
function isAcyclicUndirected(graph) {
|
|
@@ -1562,37 +1360,18 @@ function isAcyclicUndirected(graph) {
|
|
|
1562
1360
|
}
|
|
1563
1361
|
return false;
|
|
1564
1362
|
};
|
|
1565
|
-
for (const
|
|
1363
|
+
for (const node of graph.nodes) if (!visited.has(node.id) && hasCycle(node.id, null)) return false;
|
|
1566
1364
|
return true;
|
|
1567
1365
|
}
|
|
1568
|
-
/**
|
|
1569
|
-
* Returns connected components as arrays of nodes.
|
|
1570
|
-
* Treats all edges as undirected for connectivity.
|
|
1571
|
-
*
|
|
1572
|
-
* **O(V + E)** time.
|
|
1573
|
-
*
|
|
1574
|
-
* @example
|
|
1575
|
-
* ```ts
|
|
1576
|
-
* import { createGraph, getConnectedComponents } from '@statelyai/graph';
|
|
1577
|
-
*
|
|
1578
|
-
* const graph = createGraph({
|
|
1579
|
-
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1580
|
-
* edges: [{ id: 'ab', sourceId: 'a', targetId: 'b' }],
|
|
1581
|
-
* });
|
|
1582
|
-
*
|
|
1583
|
-
* const components = getConnectedComponents(graph);
|
|
1584
|
-
* // [[nodeA, nodeB], [nodeC]]
|
|
1585
|
-
* ```
|
|
1586
|
-
*/
|
|
1587
1366
|
function getConnectedComponents(graph) {
|
|
1588
1367
|
const idx = getIndex(graph);
|
|
1589
1368
|
const visited = /* @__PURE__ */ new Set();
|
|
1590
1369
|
const components = [];
|
|
1591
|
-
for (const
|
|
1592
|
-
if (visited.has(
|
|
1370
|
+
for (const node of graph.nodes) {
|
|
1371
|
+
if (visited.has(node.id)) continue;
|
|
1593
1372
|
const component = [];
|
|
1594
|
-
const queue = [
|
|
1595
|
-
visited.add(
|
|
1373
|
+
const queue = [node.id];
|
|
1374
|
+
visited.add(node.id);
|
|
1596
1375
|
while (queue.length > 0) {
|
|
1597
1376
|
const id = queue.shift();
|
|
1598
1377
|
const ni = idx.nodeById.get(id);
|
|
@@ -1620,34 +1399,13 @@ function getConnectedComponents(graph) {
|
|
|
1620
1399
|
}
|
|
1621
1400
|
return components;
|
|
1622
1401
|
}
|
|
1623
|
-
/**
|
|
1624
|
-
* Returns a topological ordering of nodes, or `null` if the graph is cyclic.
|
|
1625
|
-
*
|
|
1626
|
-
* **O(V + E)** time (Kahn's algorithm).
|
|
1627
|
-
*
|
|
1628
|
-
* @example
|
|
1629
|
-
* ```ts
|
|
1630
|
-
* import { createGraph, getTopologicalSort } from '@statelyai/graph';
|
|
1631
|
-
*
|
|
1632
|
-
* const graph = createGraph({
|
|
1633
|
-
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1634
|
-
* edges: [
|
|
1635
|
-
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1636
|
-
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
1637
|
-
* ],
|
|
1638
|
-
* });
|
|
1639
|
-
*
|
|
1640
|
-
* const sorted = getTopologicalSort(graph);
|
|
1641
|
-
* // [nodeA, nodeB, nodeC]
|
|
1642
|
-
* ```
|
|
1643
|
-
*/
|
|
1644
1402
|
function getTopologicalSort(graph) {
|
|
1645
1403
|
const idx = getIndex(graph);
|
|
1646
|
-
const
|
|
1647
|
-
for (const
|
|
1648
|
-
for (const
|
|
1404
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
1405
|
+
for (const node of graph.nodes) inDegree.set(node.id, 0);
|
|
1406
|
+
for (const edge of graph.edges) inDegree.set(edge.targetId, (inDegree.get(edge.targetId) ?? 0) + 1);
|
|
1649
1407
|
const queue = [];
|
|
1650
|
-
for (const [id,
|
|
1408
|
+
for (const [id, degree] of inDegree) if (degree === 0) queue.push(id);
|
|
1651
1409
|
const result = [];
|
|
1652
1410
|
while (queue.length > 0) {
|
|
1653
1411
|
const id = queue.shift();
|
|
@@ -1657,1274 +1415,976 @@ function getTopologicalSort(graph) {
|
|
|
1657
1415
|
const ai = idx.edgeById.get(eid);
|
|
1658
1416
|
if (ai === void 0) continue;
|
|
1659
1417
|
const targetId = graph.edges[ai].targetId;
|
|
1660
|
-
const
|
|
1661
|
-
|
|
1662
|
-
if (
|
|
1418
|
+
const nextDegree = (inDegree.get(targetId) ?? 1) - 1;
|
|
1419
|
+
inDegree.set(targetId, nextDegree);
|
|
1420
|
+
if (nextDegree === 0) queue.push(targetId);
|
|
1663
1421
|
}
|
|
1664
1422
|
}
|
|
1665
1423
|
if (result.length !== graph.nodes.length) return null;
|
|
1666
1424
|
return result;
|
|
1667
1425
|
}
|
|
1668
|
-
/**
|
|
1669
|
-
* Checks whether a path exists between two nodes.
|
|
1670
|
-
*
|
|
1671
|
-
* **O(V + E)** time (BFS) or **O((V + E) log V)** (Dijkstra when weighted).
|
|
1672
|
-
*
|
|
1673
|
-
* @example
|
|
1674
|
-
* ```ts
|
|
1675
|
-
* import { createGraph, hasPath } from '@statelyai/graph';
|
|
1676
|
-
*
|
|
1677
|
-
* const graph = createGraph({
|
|
1678
|
-
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1679
|
-
* edges: [{ id: 'ab', sourceId: 'a', targetId: 'b' }],
|
|
1680
|
-
* });
|
|
1681
|
-
*
|
|
1682
|
-
* hasPath(graph, 'a', 'b'); // true
|
|
1683
|
-
* hasPath(graph, 'a', 'c'); // false
|
|
1684
|
-
* ```
|
|
1685
|
-
*/
|
|
1686
1426
|
function hasPath(graph, sourceId, targetId) {
|
|
1687
1427
|
return getShortestPaths(graph, {
|
|
1688
1428
|
from: sourceId,
|
|
1689
1429
|
to: targetId
|
|
1690
1430
|
}).length > 0;
|
|
1691
1431
|
}
|
|
1692
|
-
/**
|
|
1693
|
-
* Checks whether the graph is connected (all nodes reachable from any node).
|
|
1694
|
-
*
|
|
1695
|
-
* **O(V + E)** time.
|
|
1696
|
-
*
|
|
1697
|
-
* @example
|
|
1698
|
-
* ```ts
|
|
1699
|
-
* import { createGraph, isConnected } from '@statelyai/graph';
|
|
1700
|
-
*
|
|
1701
|
-
* const graph = createGraph({
|
|
1702
|
-
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
1703
|
-
* edges: [{ id: 'ab', sourceId: 'a', targetId: 'b' }],
|
|
1704
|
-
* });
|
|
1705
|
-
*
|
|
1706
|
-
* isConnected(graph); // true
|
|
1707
|
-
* ```
|
|
1708
|
-
*/
|
|
1709
1432
|
function isConnected(graph) {
|
|
1710
1433
|
if (graph.nodes.length === 0) return true;
|
|
1711
1434
|
return getConnectedComponents(graph).length <= 1;
|
|
1712
1435
|
}
|
|
1713
|
-
/**
|
|
1714
|
-
* Checks whether the graph is a tree (connected and acyclic).
|
|
1715
|
-
*
|
|
1716
|
-
* **O(V + E)** time.
|
|
1717
|
-
*
|
|
1718
|
-
* @example
|
|
1719
|
-
* ```ts
|
|
1720
|
-
* import { createGraph, isTree } from '@statelyai/graph';
|
|
1721
|
-
*
|
|
1722
|
-
* const tree = createGraph({
|
|
1723
|
-
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1724
|
-
* edges: [
|
|
1725
|
-
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1726
|
-
* { id: 'ac', sourceId: 'a', targetId: 'c' },
|
|
1727
|
-
* ],
|
|
1728
|
-
* });
|
|
1729
|
-
*
|
|
1730
|
-
* isTree(tree); // true
|
|
1731
|
-
* ```
|
|
1732
|
-
*/
|
|
1733
1436
|
function isTree(graph) {
|
|
1734
1437
|
return isConnected(graph) && isAcyclic(graph);
|
|
1735
1438
|
}
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
const inDeg = /* @__PURE__ */ new Map();
|
|
1741
|
-
for (const n of graph.nodes) inDeg.set(n.id, 0);
|
|
1742
|
-
for (const e of graph.edges) inDeg.set(e.targetId, (inDeg.get(e.targetId) ?? 0) + 1);
|
|
1743
|
-
const roots = [...inDeg.entries()].filter(([, d]) => d === 0).map(([id]) => id);
|
|
1744
|
-
if (roots.length === 1) return roots[0];
|
|
1745
|
-
throw new Error("Cannot determine start node — provide opts.from or set graph.initialNodeId");
|
|
1746
|
-
}
|
|
1747
|
-
/** Get neighbor IDs with their connecting edges. */
|
|
1748
|
-
function getNeighborEdges(graph, nodeId) {
|
|
1439
|
+
|
|
1440
|
+
//#endregion
|
|
1441
|
+
//#region src/algorithms/ordering.ts
|
|
1442
|
+
function getPreorder(graph, opts) {
|
|
1749
1443
|
const idx = getIndex(graph);
|
|
1750
|
-
const
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1444
|
+
const startId = resolveFrom(graph, opts);
|
|
1445
|
+
const startNi = idx.nodeById.get(startId);
|
|
1446
|
+
if (startNi === void 0) return [];
|
|
1447
|
+
const visited = new Set([startId]);
|
|
1448
|
+
const result = [graph.nodes[startNi]];
|
|
1449
|
+
const stack = [startId];
|
|
1450
|
+
while (stack.length > 0) {
|
|
1451
|
+
const top = stack[stack.length - 1];
|
|
1452
|
+
const next = getNeighborIds$1(graph, top).find((id) => !visited.has(id));
|
|
1453
|
+
if (next === void 0) {
|
|
1454
|
+
stack.pop();
|
|
1455
|
+
continue;
|
|
1759
1456
|
}
|
|
1457
|
+
visited.add(next);
|
|
1458
|
+
stack.push(next);
|
|
1459
|
+
const ni = idx.nodeById.get(next);
|
|
1460
|
+
if (ni !== void 0) result.push(graph.nodes[ni]);
|
|
1760
1461
|
}
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1462
|
+
return result;
|
|
1463
|
+
}
|
|
1464
|
+
function getPostorder(graph, opts) {
|
|
1465
|
+
const idx = getIndex(graph);
|
|
1466
|
+
const startId = resolveFrom(graph, opts);
|
|
1467
|
+
if (idx.nodeById.get(startId) === void 0) return [];
|
|
1468
|
+
const visited = new Set([startId]);
|
|
1469
|
+
const result = [];
|
|
1470
|
+
const stack = [startId];
|
|
1471
|
+
while (stack.length > 0) {
|
|
1472
|
+
const top = stack[stack.length - 1];
|
|
1473
|
+
const next = getNeighborIds$1(graph, top).find((id) => !visited.has(id));
|
|
1474
|
+
if (next === void 0) {
|
|
1475
|
+
stack.pop();
|
|
1476
|
+
const ni = idx.nodeById.get(top);
|
|
1477
|
+
if (ni !== void 0) result.push(graph.nodes[ni]);
|
|
1478
|
+
continue;
|
|
1769
1479
|
}
|
|
1480
|
+
visited.add(next);
|
|
1481
|
+
stack.push(next);
|
|
1770
1482
|
}
|
|
1771
1483
|
return result;
|
|
1772
1484
|
}
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
function
|
|
1780
|
-
const
|
|
1781
|
-
const
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
if (!
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1485
|
+
function getPreorders(graph, opts) {
|
|
1486
|
+
return [...genPreorders(graph, opts)];
|
|
1487
|
+
}
|
|
1488
|
+
function getPostorders(graph, opts) {
|
|
1489
|
+
return [...genPostorders(graph, opts)];
|
|
1490
|
+
}
|
|
1491
|
+
function* genPreorders(graph, opts) {
|
|
1492
|
+
const idx = getIndex(graph);
|
|
1493
|
+
const startId = resolveFrom(graph, opts);
|
|
1494
|
+
const startNi = idx.nodeById.get(startId);
|
|
1495
|
+
const startNode = startNi !== void 0 ? graph.nodes[startNi] : void 0;
|
|
1496
|
+
if (!startNode) return;
|
|
1497
|
+
const queue = [{
|
|
1498
|
+
visited: new Set([startId]),
|
|
1499
|
+
preorder: [startNode],
|
|
1500
|
+
dfsStack: [startId]
|
|
1501
|
+
}];
|
|
1502
|
+
while (queue.length > 0) {
|
|
1503
|
+
const frame = queue.pop();
|
|
1504
|
+
const { visited, dfsStack } = frame;
|
|
1505
|
+
let { preorder } = frame;
|
|
1506
|
+
let branched = false;
|
|
1507
|
+
while (dfsStack.length > 0) {
|
|
1508
|
+
const top = dfsStack[dfsStack.length - 1];
|
|
1509
|
+
const unvisited = getNeighborIds$1(graph, top).filter((id) => !visited.has(id));
|
|
1510
|
+
if (unvisited.length === 0) {
|
|
1511
|
+
dfsStack.pop();
|
|
1512
|
+
continue;
|
|
1513
|
+
}
|
|
1514
|
+
for (const nextId of unvisited) {
|
|
1515
|
+
const ni = idx.nodeById.get(nextId);
|
|
1516
|
+
if (ni === void 0) continue;
|
|
1517
|
+
const newVisited = new Set(visited);
|
|
1518
|
+
newVisited.add(nextId);
|
|
1519
|
+
queue.push({
|
|
1520
|
+
visited: newVisited,
|
|
1521
|
+
preorder: [...preorder, graph.nodes[ni]],
|
|
1522
|
+
dfsStack: [...dfsStack, nextId]
|
|
1802
1523
|
});
|
|
1803
1524
|
}
|
|
1525
|
+
branched = true;
|
|
1526
|
+
break;
|
|
1804
1527
|
}
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1528
|
+
if (!branched) yield preorder;
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
function* genPostorders(graph, opts) {
|
|
1532
|
+
const idx = getIndex(graph);
|
|
1533
|
+
const startId = resolveFrom(graph, opts);
|
|
1534
|
+
if (idx.nodeById.get(startId) === void 0) return;
|
|
1535
|
+
const queue = [{
|
|
1536
|
+
visited: new Set([startId]),
|
|
1537
|
+
postorder: [],
|
|
1538
|
+
dfsStack: [startId]
|
|
1539
|
+
}];
|
|
1540
|
+
while (queue.length > 0) {
|
|
1541
|
+
const frame = queue.pop();
|
|
1542
|
+
const { visited, dfsStack } = frame;
|
|
1543
|
+
let { postorder } = frame;
|
|
1544
|
+
let branched = false;
|
|
1545
|
+
while (dfsStack.length > 0) {
|
|
1546
|
+
const top = dfsStack[dfsStack.length - 1];
|
|
1547
|
+
const unvisited = getNeighborIds$1(graph, top).filter((id) => !visited.has(id));
|
|
1548
|
+
if (unvisited.length === 0) {
|
|
1549
|
+
dfsStack.pop();
|
|
1550
|
+
const ni = idx.nodeById.get(top);
|
|
1551
|
+
if (ni !== void 0) postorder = [...postorder, graph.nodes[ni]];
|
|
1552
|
+
continue;
|
|
1553
|
+
}
|
|
1554
|
+
for (const nextId of unvisited) {
|
|
1555
|
+
if (idx.nodeById.get(nextId) === void 0) continue;
|
|
1556
|
+
const newVisited = new Set(visited);
|
|
1557
|
+
newVisited.add(nextId);
|
|
1558
|
+
queue.push({
|
|
1559
|
+
visited: newVisited,
|
|
1560
|
+
postorder: [...postorder],
|
|
1561
|
+
dfsStack: [...dfsStack, nextId]
|
|
1833
1562
|
});
|
|
1834
1563
|
}
|
|
1564
|
+
branched = true;
|
|
1565
|
+
break;
|
|
1835
1566
|
}
|
|
1567
|
+
if (!branched) yield postorder;
|
|
1836
1568
|
}
|
|
1837
|
-
return {
|
|
1838
|
-
dist,
|
|
1839
|
-
prev
|
|
1840
|
-
};
|
|
1841
1569
|
}
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1570
|
+
|
|
1571
|
+
//#endregion
|
|
1572
|
+
//#region src/algorithms/spanning-tree.ts
|
|
1573
|
+
function getMinimumSpanningTree(graph, opts) {
|
|
1574
|
+
const algorithm = opts?.algorithm ?? "prim";
|
|
1575
|
+
const getWeight = opts?.getWeight ?? ((edge) => edge.weight ?? 1);
|
|
1576
|
+
const mstEdges = algorithm === "kruskal" ? kruskalMST(graph, getWeight) : primMST(graph, getWeight);
|
|
1577
|
+
return createGraph({
|
|
1578
|
+
id: graph.id,
|
|
1579
|
+
type: graph.type,
|
|
1580
|
+
initialNodeId: graph.initialNodeId ?? void 0,
|
|
1581
|
+
nodes: graph.nodes.map((node) => ({
|
|
1582
|
+
id: node.id,
|
|
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
|
+
}))
|
|
1596
|
+
});
|
|
1862
1597
|
}
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
* Use `getShortestPaths` for the full array.
|
|
1866
|
-
*
|
|
1867
|
-
* **O(V + E)** time (BFS) or **O((V + E) log V)** (Dijkstra when weighted),
|
|
1868
|
-
* plus **O(P)** per path yielded where P is the path length.
|
|
1869
|
-
*
|
|
1870
|
-
* @example
|
|
1871
|
-
* ```ts
|
|
1872
|
-
* import { createGraph, genShortestPaths } from '@statelyai/graph';
|
|
1873
|
-
*
|
|
1874
|
-
* const graph = createGraph({
|
|
1875
|
-
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1876
|
-
* edges: [
|
|
1877
|
-
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1878
|
-
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
1879
|
-
* ],
|
|
1880
|
-
* initialNodeId: 'a',
|
|
1881
|
-
* });
|
|
1882
|
-
*
|
|
1883
|
-
* for (const path of genShortestPaths(graph)) {
|
|
1884
|
-
* console.log(path.steps.map(s => s.node.id));
|
|
1885
|
-
* }
|
|
1886
|
-
* ```
|
|
1887
|
-
*/
|
|
1888
|
-
function* genShortestPaths(graph, opts) {
|
|
1598
|
+
function primMST(graph, getWeight) {
|
|
1599
|
+
if (graph.nodes.length === 0) return [];
|
|
1889
1600
|
const idx = getIndex(graph);
|
|
1890
|
-
const
|
|
1891
|
-
const
|
|
1892
|
-
const
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1601
|
+
const inMST = /* @__PURE__ */ new Set();
|
|
1602
|
+
const mstEdges = [];
|
|
1603
|
+
const candidates = new MinPriorityQueue((a, b) => a.weight - b.weight);
|
|
1604
|
+
function addEdgesOf(nodeId) {
|
|
1605
|
+
for (const eid of idx.outEdges.get(nodeId) ?? []) {
|
|
1606
|
+
const ai = idx.edgeById.get(eid);
|
|
1607
|
+
if (ai === void 0) continue;
|
|
1608
|
+
const edge = graph.edges[ai];
|
|
1609
|
+
if (!inMST.has(edge.targetId)) candidates.push({
|
|
1610
|
+
weight: getWeight(edge),
|
|
1611
|
+
edge
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1614
|
+
if (graph.type === "undirected") for (const eid of idx.inEdges.get(nodeId) ?? []) {
|
|
1615
|
+
const ai = idx.edgeById.get(eid);
|
|
1616
|
+
if (ai === void 0) continue;
|
|
1617
|
+
const edge = graph.edges[ai];
|
|
1618
|
+
if (!inMST.has(edge.sourceId)) candidates.push({
|
|
1619
|
+
weight: getWeight(edge),
|
|
1620
|
+
edge
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
const startId = graph.nodes[0].id;
|
|
1625
|
+
inMST.add(startId);
|
|
1626
|
+
addEdgesOf(startId);
|
|
1627
|
+
while (candidates.size > 0 && inMST.size < graph.nodes.length) {
|
|
1628
|
+
const { edge } = candidates.pop();
|
|
1629
|
+
const targetId = graph.type === "undirected" && inMST.has(edge.targetId) ? edge.sourceId : edge.targetId;
|
|
1630
|
+
if (inMST.has(targetId)) continue;
|
|
1631
|
+
inMST.add(targetId);
|
|
1632
|
+
mstEdges.push(edge);
|
|
1633
|
+
addEdgesOf(targetId);
|
|
1634
|
+
}
|
|
1635
|
+
return mstEdges;
|
|
1896
1636
|
}
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1637
|
+
function kruskalMST(graph, getWeight) {
|
|
1638
|
+
const sorted = [...graph.edges].sort((a, b) => getWeight(a) - getWeight(b));
|
|
1639
|
+
const parent = /* @__PURE__ */ new Map();
|
|
1640
|
+
const rank = /* @__PURE__ */ new Map();
|
|
1641
|
+
for (const node of graph.nodes) {
|
|
1642
|
+
parent.set(node.id, node.id);
|
|
1643
|
+
rank.set(node.id, 0);
|
|
1644
|
+
}
|
|
1645
|
+
function find(id) {
|
|
1646
|
+
if (parent.get(id) !== id) parent.set(id, find(parent.get(id)));
|
|
1647
|
+
return parent.get(id);
|
|
1648
|
+
}
|
|
1649
|
+
function union(a, b) {
|
|
1650
|
+
const rootA = find(a);
|
|
1651
|
+
const rootB = find(b);
|
|
1652
|
+
if (rootA === rootB) return false;
|
|
1653
|
+
if (rank.get(rootA) < rank.get(rootB)) parent.set(rootA, rootB);
|
|
1654
|
+
else if (rank.get(rootA) > rank.get(rootB)) parent.set(rootB, rootA);
|
|
1655
|
+
else {
|
|
1656
|
+
parent.set(rootB, rootA);
|
|
1657
|
+
rank.set(rootA, rank.get(rootA) + 1);
|
|
1658
|
+
}
|
|
1659
|
+
return true;
|
|
1660
|
+
}
|
|
1661
|
+
const mstEdges = [];
|
|
1662
|
+
for (const edge of sorted) if (union(edge.sourceId, edge.targetId)) mstEdges.push(edge);
|
|
1663
|
+
return mstEdges;
|
|
1922
1664
|
}
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
* @example
|
|
1929
|
-
* ```ts
|
|
1930
|
-
* import { createGraph, getShortestPath } from '@statelyai/graph';
|
|
1931
|
-
*
|
|
1932
|
-
* const graph = createGraph({
|
|
1933
|
-
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1934
|
-
* edges: [
|
|
1935
|
-
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1936
|
-
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
1937
|
-
* ],
|
|
1938
|
-
* initialNodeId: 'a',
|
|
1939
|
-
* });
|
|
1940
|
-
*
|
|
1941
|
-
* const path = getShortestPath(graph, { to: 'c' });
|
|
1942
|
-
* // path.steps -> [{node: nodeB, edge: ...}, {node: nodeC, edge: ...}]
|
|
1943
|
-
* ```
|
|
1944
|
-
*/
|
|
1945
|
-
function getShortestPath(graph, opts) {
|
|
1946
|
-
for (const path of genShortestPaths(graph, opts)) return path;
|
|
1665
|
+
|
|
1666
|
+
//#endregion
|
|
1667
|
+
//#region src/algorithms/centrality.ts
|
|
1668
|
+
function getNodeIds(graph) {
|
|
1669
|
+
return graph.nodes.map((node) => node.id);
|
|
1947
1670
|
}
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
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
|
+
function createEmptyScoreMap(graph) {
|
|
1698
|
+
return Object.fromEntries(graph.nodes.map((node) => [node.id, 0]));
|
|
1699
|
+
}
|
|
1700
|
+
function normalizeVector(scores) {
|
|
1701
|
+
const magnitude = Math.sqrt(Object.values(scores).reduce((sum, value) => sum + value * value, 0));
|
|
1702
|
+
if (magnitude === 0) return scores;
|
|
1703
|
+
for (const key of Object.keys(scores)) scores[key] /= magnitude;
|
|
1704
|
+
return scores;
|
|
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;
|
|
1710
|
+
}
|
|
1711
|
+
function getReachableDistances(graph, startId) {
|
|
1712
|
+
const distances = /* @__PURE__ */ new Map();
|
|
1713
|
+
const queue = [startId];
|
|
1714
|
+
distances.set(startId, 0);
|
|
1715
|
+
while (queue.length > 0) {
|
|
1716
|
+
const currentId = queue.shift();
|
|
1717
|
+
const currentDistance = distances.get(currentId);
|
|
1718
|
+
for (const neighborId of getNeighborIds(graph, currentId)) {
|
|
1719
|
+
if (distances.has(neighborId)) continue;
|
|
1720
|
+
distances.set(neighborId, currentDistance + 1);
|
|
1721
|
+
queue.push(neighborId);
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
return distances;
|
|
1974
1725
|
}
|
|
1975
1726
|
/**
|
|
1976
|
-
*
|
|
1977
|
-
* Use `getSimplePaths` for the full array.
|
|
1727
|
+
* Returns degree centrality scores for all nodes.
|
|
1978
1728
|
*
|
|
1979
|
-
*
|
|
1729
|
+
* Degree centrality is the node degree normalized by `n - 1`.
|
|
1980
1730
|
*
|
|
1981
1731
|
* @example
|
|
1982
1732
|
* ```ts
|
|
1983
|
-
*
|
|
1984
|
-
*
|
|
1985
|
-
* const graph = createGraph({
|
|
1986
|
-
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1987
|
-
* edges: [
|
|
1988
|
-
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1989
|
-
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
1990
|
-
* { id: 'ac', sourceId: 'a', targetId: 'c' },
|
|
1991
|
-
* ],
|
|
1992
|
-
* initialNodeId: 'a',
|
|
1993
|
-
* });
|
|
1994
|
-
*
|
|
1995
|
-
* for (const path of genSimplePaths(graph, { to: 'c' })) {
|
|
1996
|
-
* console.log(path.steps.map(s => s.node.id));
|
|
1997
|
-
* // ['b', 'c'] or ['c']
|
|
1998
|
-
* }
|
|
1733
|
+
* const scores = getDegreeCentrality(graph);
|
|
1734
|
+
* console.log(scores.a); // 0.5
|
|
1999
1735
|
* ```
|
|
2000
1736
|
*/
|
|
2001
|
-
function
|
|
1737
|
+
function getDegreeCentrality(graph) {
|
|
1738
|
+
const scale = graph.nodes.length > 1 ? 1 / (graph.nodes.length - 1) : 0;
|
|
2002
1739
|
const idx = getIndex(graph);
|
|
2003
|
-
const
|
|
2004
|
-
const
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
function* dfsCollect(nodeId) {
|
|
2010
|
-
visited.add(nodeId);
|
|
2011
|
-
if (targetId !== void 0) {
|
|
2012
|
-
if (nodeId === targetId) {
|
|
2013
|
-
yield {
|
|
2014
|
-
source: sourceNode,
|
|
2015
|
-
steps: [...currentSteps]
|
|
2016
|
-
};
|
|
2017
|
-
visited.delete(nodeId);
|
|
2018
|
-
return;
|
|
2019
|
-
}
|
|
2020
|
-
} else if (currentSteps.length > 0) yield {
|
|
2021
|
-
source: sourceNode,
|
|
2022
|
-
steps: [...currentSteps]
|
|
2023
|
-
};
|
|
2024
|
-
for (const { neighborId, edge } of getNeighborEdges(graph, nodeId)) if (!visited.has(neighborId)) {
|
|
2025
|
-
const neighborNi = idx.nodeById.get(neighborId);
|
|
2026
|
-
const neighborNode = neighborNi !== void 0 ? graph.nodes[neighborNi] : graph.nodes.find((n) => n.id === neighborId);
|
|
2027
|
-
currentSteps.push({
|
|
2028
|
-
edge,
|
|
2029
|
-
node: neighborNode
|
|
2030
|
-
});
|
|
2031
|
-
yield* dfsCollect(neighborId);
|
|
2032
|
-
currentSteps.pop();
|
|
2033
|
-
}
|
|
2034
|
-
visited.delete(nodeId);
|
|
1740
|
+
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;
|
|
2035
1746
|
}
|
|
2036
|
-
|
|
1747
|
+
return scores;
|
|
2037
1748
|
}
|
|
2038
1749
|
/**
|
|
2039
|
-
* Returns
|
|
2040
|
-
*
|
|
2041
|
-
* **O(V + E)** typical, **O(V!)** worst-case.
|
|
2042
|
-
*
|
|
2043
|
-
* @example
|
|
2044
|
-
* ```ts
|
|
2045
|
-
* import { createGraph, getSimplePath } from '@statelyai/graph';
|
|
2046
|
-
*
|
|
2047
|
-
* const graph = createGraph({
|
|
2048
|
-
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
2049
|
-
* edges: [
|
|
2050
|
-
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
2051
|
-
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
2052
|
-
* ],
|
|
2053
|
-
* initialNodeId: 'a',
|
|
2054
|
-
* });
|
|
1750
|
+
* Returns in-degree centrality scores for all nodes.
|
|
2055
1751
|
*
|
|
2056
|
-
*
|
|
2057
|
-
* // path.steps -> [{node: nodeB, edge: ...}, {node: nodeC, edge: ...}]
|
|
2058
|
-
* ```
|
|
1752
|
+
* In-degree centrality is the incoming degree normalized by `n - 1`.
|
|
2059
1753
|
*/
|
|
2060
|
-
function
|
|
2061
|
-
|
|
1754
|
+
function getInDegreeCentrality(graph) {
|
|
1755
|
+
const scale = graph.nodes.length > 1 ? 1 / (graph.nodes.length - 1) : 0;
|
|
1756
|
+
const idx = getIndex(graph);
|
|
1757
|
+
const scores = createEmptyScoreMap(graph);
|
|
1758
|
+
for (const node of graph.nodes) scores[node.id] = (idx.inEdges.get(node.id)?.length ?? 0) * scale;
|
|
1759
|
+
return scores;
|
|
2062
1760
|
}
|
|
2063
1761
|
/**
|
|
2064
|
-
* Returns
|
|
2065
|
-
* Only meaningful for directed graphs.
|
|
2066
|
-
*
|
|
2067
|
-
* **O(V + E)** time.
|
|
2068
|
-
*
|
|
2069
|
-
* @example
|
|
2070
|
-
* ```ts
|
|
2071
|
-
* import { createGraph, getStronglyConnectedComponents } from '@statelyai/graph';
|
|
2072
|
-
*
|
|
2073
|
-
* const graph = createGraph({
|
|
2074
|
-
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
2075
|
-
* edges: [
|
|
2076
|
-
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
2077
|
-
* { id: 'ba', sourceId: 'b', targetId: 'a' },
|
|
2078
|
-
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
2079
|
-
* ],
|
|
2080
|
-
* });
|
|
1762
|
+
* Returns out-degree centrality scores for all nodes.
|
|
2081
1763
|
*
|
|
2082
|
-
*
|
|
2083
|
-
* // [[nodeA, nodeB], [nodeC]]
|
|
2084
|
-
* ```
|
|
1764
|
+
* Out-degree centrality is the outgoing degree normalized by `n - 1`.
|
|
2085
1765
|
*/
|
|
2086
|
-
function
|
|
1766
|
+
function getOutDegreeCentrality(graph) {
|
|
1767
|
+
const scale = graph.nodes.length > 1 ? 1 / (graph.nodes.length - 1) : 0;
|
|
2087
1768
|
const idx = getIndex(graph);
|
|
2088
|
-
|
|
2089
|
-
const
|
|
2090
|
-
|
|
2091
|
-
const onStack = /* @__PURE__ */ new Set();
|
|
2092
|
-
const stack = [];
|
|
2093
|
-
const result = [];
|
|
2094
|
-
function strongconnect(id) {
|
|
2095
|
-
nodeIndex.set(id, indexCounter);
|
|
2096
|
-
lowlink.set(id, indexCounter);
|
|
2097
|
-
indexCounter++;
|
|
2098
|
-
stack.push(id);
|
|
2099
|
-
onStack.add(id);
|
|
2100
|
-
for (const eid of idx.outEdges.get(id) ?? []) {
|
|
2101
|
-
const ai = idx.edgeById.get(eid);
|
|
2102
|
-
if (ai === void 0) continue;
|
|
2103
|
-
const wId = graph.edges[ai].targetId;
|
|
2104
|
-
if (!nodeIndex.has(wId)) {
|
|
2105
|
-
strongconnect(wId);
|
|
2106
|
-
lowlink.set(id, Math.min(lowlink.get(id), lowlink.get(wId)));
|
|
2107
|
-
} else if (onStack.has(wId)) lowlink.set(id, Math.min(lowlink.get(id), nodeIndex.get(wId)));
|
|
2108
|
-
}
|
|
2109
|
-
if (lowlink.get(id) === nodeIndex.get(id)) {
|
|
2110
|
-
const component = [];
|
|
2111
|
-
let wId;
|
|
2112
|
-
do {
|
|
2113
|
-
wId = stack.pop();
|
|
2114
|
-
onStack.delete(wId);
|
|
2115
|
-
const ni = idx.nodeById.get(wId);
|
|
2116
|
-
if (ni !== void 0) component.push(graph.nodes[ni]);
|
|
2117
|
-
} while (wId !== id);
|
|
2118
|
-
result.push(component);
|
|
2119
|
-
}
|
|
2120
|
-
}
|
|
2121
|
-
for (const n of graph.nodes) if (!nodeIndex.has(n.id)) strongconnect(n.id);
|
|
2122
|
-
return result;
|
|
1769
|
+
const scores = createEmptyScoreMap(graph);
|
|
1770
|
+
for (const node of graph.nodes) scores[node.id] = (idx.outEdges.get(node.id)?.length ?? 0) * scale;
|
|
1771
|
+
return scores;
|
|
2123
1772
|
}
|
|
2124
1773
|
/**
|
|
2125
|
-
* Returns
|
|
2126
|
-
* Delegates to `genCycles` internally.
|
|
2127
|
-
*
|
|
2128
|
-
* **O((V + E) · C)** where C is the number of elementary cycles (can be exponential).
|
|
2129
|
-
*
|
|
2130
|
-
* @example
|
|
2131
|
-
* ```ts
|
|
2132
|
-
* import { createGraph, getCycles } from '@statelyai/graph';
|
|
2133
|
-
*
|
|
2134
|
-
* const graph = createGraph({
|
|
2135
|
-
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
2136
|
-
* edges: [
|
|
2137
|
-
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
2138
|
-
* { id: 'ba', sourceId: 'b', targetId: 'a' },
|
|
2139
|
-
* ],
|
|
2140
|
-
* });
|
|
1774
|
+
* Returns closeness centrality scores for all nodes.
|
|
2141
1775
|
*
|
|
2142
|
-
*
|
|
2143
|
-
*
|
|
2144
|
-
* ```
|
|
1776
|
+
* Distances are computed over unweighted shortest paths using the graph's
|
|
1777
|
+
* existing directed or undirected edge semantics.
|
|
2145
1778
|
*/
|
|
2146
|
-
function
|
|
2147
|
-
|
|
1779
|
+
function getClosenessCentrality(graph) {
|
|
1780
|
+
const scores = createEmptyScoreMap(graph);
|
|
1781
|
+
const order = graph.nodes.length;
|
|
1782
|
+
for (const node of graph.nodes) {
|
|
1783
|
+
const distances = getReachableDistances(graph, node.id);
|
|
1784
|
+
distances.delete(node.id);
|
|
1785
|
+
if (distances.size === 0) continue;
|
|
1786
|
+
const totalDistance = [...distances.values()].reduce((sum, distance) => sum + distance, 0);
|
|
1787
|
+
if (totalDistance === 0) continue;
|
|
1788
|
+
const reachable = distances.size;
|
|
1789
|
+
const closeness = reachable / totalDistance;
|
|
1790
|
+
scores[node.id] = order > 1 ? closeness * (reachable / (order - 1)) : closeness;
|
|
1791
|
+
}
|
|
1792
|
+
return scores;
|
|
2148
1793
|
}
|
|
2149
1794
|
/**
|
|
2150
|
-
*
|
|
2151
|
-
* Use `getCycles` for the full array.
|
|
2152
|
-
*
|
|
2153
|
-
* **O((V + E) · C)** where C is the number of elementary cycles (can be exponential).
|
|
2154
|
-
*
|
|
2155
|
-
* @example
|
|
2156
|
-
* ```ts
|
|
2157
|
-
* import { createGraph, genCycles } from '@statelyai/graph';
|
|
2158
|
-
*
|
|
2159
|
-
* const graph = createGraph({
|
|
2160
|
-
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
2161
|
-
* edges: [
|
|
2162
|
-
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
2163
|
-
* { id: 'ba', sourceId: 'b', targetId: 'a' },
|
|
2164
|
-
* ],
|
|
2165
|
-
* });
|
|
1795
|
+
* Returns betweenness centrality scores for all nodes.
|
|
2166
1796
|
*
|
|
2167
|
-
*
|
|
2168
|
-
*
|
|
2169
|
-
* }
|
|
2170
|
-
* ```
|
|
1797
|
+
* Uses Brandes' algorithm over unweighted shortest paths and returns
|
|
1798
|
+
* normalized scores.
|
|
2171
1799
|
*/
|
|
2172
|
-
function
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
const
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
const startNi = idx.nodeById.get(startId);
|
|
2185
|
-
const startNode = graph.nodes[startNi];
|
|
2186
|
-
const found = [];
|
|
2187
|
-
function dfsFind(currentId) {
|
|
2188
|
-
visited.add(currentId);
|
|
2189
|
-
for (const eid of idx.outEdges.get(currentId) ?? []) {
|
|
2190
|
-
const ai = idx.edgeById.get(eid);
|
|
2191
|
-
if (ai === void 0) continue;
|
|
2192
|
-
const e = graph.edges[ai];
|
|
2193
|
-
const neighborId = e.targetId;
|
|
2194
|
-
if (neighborId === startId && (steps.length > 0 || currentId === startId)) found.push({
|
|
2195
|
-
source: startNode,
|
|
2196
|
-
steps: [...steps, {
|
|
2197
|
-
edge: e,
|
|
2198
|
-
node: startNode
|
|
2199
|
-
}]
|
|
2200
|
-
});
|
|
2201
|
-
else if (allowed.has(neighborId) && !visited.has(neighborId)) {
|
|
2202
|
-
const ni = idx.nodeById.get(neighborId);
|
|
2203
|
-
steps.push({
|
|
2204
|
-
edge: e,
|
|
2205
|
-
node: graph.nodes[ni]
|
|
2206
|
-
});
|
|
2207
|
-
dfsFind(neighborId);
|
|
2208
|
-
steps.pop();
|
|
2209
|
-
}
|
|
2210
|
-
}
|
|
2211
|
-
visited.delete(currentId);
|
|
1800
|
+
function getBetweennessCentrality(graph) {
|
|
1801
|
+
const scores = createEmptyScoreMap(graph);
|
|
1802
|
+
for (const source of graph.nodes) {
|
|
1803
|
+
const stack = [];
|
|
1804
|
+
const predecessors = /* @__PURE__ */ new Map();
|
|
1805
|
+
const sigma = /* @__PURE__ */ new Map();
|
|
1806
|
+
const distance = /* @__PURE__ */ new Map();
|
|
1807
|
+
const queue = [source.id];
|
|
1808
|
+
for (const node of graph.nodes) {
|
|
1809
|
+
predecessors.set(node.id, []);
|
|
1810
|
+
sigma.set(node.id, 0);
|
|
1811
|
+
distance.set(node.id, -1);
|
|
2212
1812
|
}
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
const startId = sortedIds[si];
|
|
2223
|
-
const allowed = new Set(sortedIds.slice(si));
|
|
2224
|
-
const visited = /* @__PURE__ */ new Set();
|
|
2225
|
-
const steps = [];
|
|
2226
|
-
const startNi = idx.nodeById.get(startId);
|
|
2227
|
-
const startNode = graph.nodes[startNi];
|
|
2228
|
-
const found = [];
|
|
2229
|
-
function dfsFind(currentId, parentId) {
|
|
2230
|
-
visited.add(currentId);
|
|
2231
|
-
for (const { neighborId, edge } of getNeighborEdgesAll(graph, currentId)) {
|
|
2232
|
-
if (neighborId === parentId) {
|
|
2233
|
-
parentId = null;
|
|
2234
|
-
continue;
|
|
1813
|
+
sigma.set(source.id, 1);
|
|
1814
|
+
distance.set(source.id, 0);
|
|
1815
|
+
while (queue.length > 0) {
|
|
1816
|
+
const currentId = queue.shift();
|
|
1817
|
+
stack.push(currentId);
|
|
1818
|
+
for (const neighborId of getNeighborIds(graph, currentId)) {
|
|
1819
|
+
if (distance.get(neighborId) === -1) {
|
|
1820
|
+
queue.push(neighborId);
|
|
1821
|
+
distance.set(neighborId, distance.get(currentId) + 1);
|
|
2235
1822
|
}
|
|
2236
|
-
if (neighborId ===
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
seen.add(innerIds);
|
|
2240
|
-
found.push({
|
|
2241
|
-
source: startNode,
|
|
2242
|
-
steps: [...steps, {
|
|
2243
|
-
edge,
|
|
2244
|
-
node: startNode
|
|
2245
|
-
}]
|
|
2246
|
-
});
|
|
2247
|
-
}
|
|
2248
|
-
} else if (allowed.has(neighborId) && !visited.has(neighborId)) {
|
|
2249
|
-
const ni = idx.nodeById.get(neighborId);
|
|
2250
|
-
steps.push({
|
|
2251
|
-
edge,
|
|
2252
|
-
node: graph.nodes[ni]
|
|
2253
|
-
});
|
|
2254
|
-
dfsFind(neighborId, currentId);
|
|
2255
|
-
steps.pop();
|
|
1823
|
+
if (distance.get(neighborId) === distance.get(currentId) + 1) {
|
|
1824
|
+
sigma.set(neighborId, sigma.get(neighborId) + sigma.get(currentId));
|
|
1825
|
+
predecessors.get(neighborId).push(currentId);
|
|
2256
1826
|
}
|
|
2257
1827
|
}
|
|
2258
|
-
visited.delete(currentId);
|
|
2259
1828
|
}
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
const e = graph.edges[ai];
|
|
2272
|
-
result.push({
|
|
2273
|
-
neighborId: e.targetId,
|
|
2274
|
-
edge: e
|
|
2275
|
-
});
|
|
1829
|
+
const delta = /* @__PURE__ */ new Map();
|
|
1830
|
+
for (const node of graph.nodes) delta.set(node.id, 0);
|
|
1831
|
+
while (stack.length > 0) {
|
|
1832
|
+
const nodeId = stack.pop();
|
|
1833
|
+
const sigmaNode = sigma.get(nodeId);
|
|
1834
|
+
if (sigmaNode === 0) continue;
|
|
1835
|
+
for (const predecessorId of predecessors.get(nodeId)) {
|
|
1836
|
+
const contribution = sigma.get(predecessorId) / sigmaNode * (1 + delta.get(nodeId));
|
|
1837
|
+
delta.set(predecessorId, delta.get(predecessorId) + contribution);
|
|
1838
|
+
}
|
|
1839
|
+
if (nodeId !== source.id) scores[nodeId] += delta.get(nodeId);
|
|
2276
1840
|
}
|
|
2277
1841
|
}
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
edge: e
|
|
2285
|
-
});
|
|
2286
|
-
}
|
|
1842
|
+
const order = graph.nodes.length;
|
|
1843
|
+
if (order <= 2) return scores;
|
|
1844
|
+
const scale = graph.type === "undirected" ? 1 / ((order - 1) * (order - 2) / 2) : 1 / ((order - 1) * (order - 2));
|
|
1845
|
+
for (const nodeId of Object.keys(scores)) {
|
|
1846
|
+
if (graph.type === "undirected") scores[nodeId] /= 2;
|
|
1847
|
+
scores[nodeId] *= scale;
|
|
2287
1848
|
}
|
|
2288
|
-
return
|
|
1849
|
+
return scores;
|
|
2289
1850
|
}
|
|
2290
1851
|
/**
|
|
2291
|
-
* Returns
|
|
2292
|
-
* Visits neighbors in the order they appear in the adjacency list.
|
|
2293
|
-
*
|
|
2294
|
-
* **O(V + E)** time.
|
|
2295
|
-
*
|
|
2296
|
-
* @example
|
|
2297
|
-
* ```ts
|
|
2298
|
-
* import { createGraph, getPreorder } from '@statelyai/graph';
|
|
2299
|
-
*
|
|
2300
|
-
* const graph = createGraph({
|
|
2301
|
-
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
2302
|
-
* edges: [
|
|
2303
|
-
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
2304
|
-
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
2305
|
-
* ],
|
|
2306
|
-
* initialNodeId: 'a',
|
|
2307
|
-
* });
|
|
1852
|
+
* Returns PageRank scores for all nodes.
|
|
2308
1853
|
*
|
|
2309
|
-
*
|
|
2310
|
-
* // [nodeA, nodeB, nodeC]
|
|
2311
|
-
* ```
|
|
1854
|
+
* Uses power iteration with damping factor `alpha`.
|
|
2312
1855
|
*/
|
|
2313
|
-
function
|
|
2314
|
-
const
|
|
2315
|
-
|
|
2316
|
-
const
|
|
2317
|
-
|
|
2318
|
-
const
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
const
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
1856
|
+
function getPageRank(graph, options) {
|
|
1857
|
+
const nodeIds = getNodeIds(graph);
|
|
1858
|
+
if (nodeIds.length === 0) return {};
|
|
1859
|
+
const alpha = options?.alpha ?? .85;
|
|
1860
|
+
const maxIterations = options?.maxIterations ?? 100;
|
|
1861
|
+
const tolerance = options?.tolerance ?? 1e-6;
|
|
1862
|
+
let scores = Object.fromEntries(nodeIds.map((nodeId) => [nodeId, 1 / nodeIds.length]));
|
|
1863
|
+
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
1864
|
+
const nextScores = Object.fromEntries(nodeIds.map((nodeId) => [nodeId, (1 - alpha) / nodeIds.length]));
|
|
1865
|
+
let danglingMass = 0;
|
|
1866
|
+
for (const nodeId of nodeIds) {
|
|
1867
|
+
const neighbors = getNeighborIds(graph, nodeId);
|
|
1868
|
+
if (neighbors.length === 0) {
|
|
1869
|
+
danglingMass += scores[nodeId];
|
|
1870
|
+
continue;
|
|
1871
|
+
}
|
|
1872
|
+
const share = scores[nodeId] / neighbors.length;
|
|
1873
|
+
for (const neighborId of neighbors) nextScores[neighborId] += alpha * share;
|
|
2327
1874
|
}
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
1875
|
+
if (danglingMass > 0) {
|
|
1876
|
+
const share = alpha * danglingMass / nodeIds.length;
|
|
1877
|
+
for (const nodeId of nodeIds) nextScores[nodeId] += share;
|
|
1878
|
+
}
|
|
1879
|
+
if (maxDiff(scores, nextScores) <= tolerance) {
|
|
1880
|
+
scores = nextScores;
|
|
1881
|
+
break;
|
|
1882
|
+
}
|
|
1883
|
+
scores = nextScores;
|
|
2332
1884
|
}
|
|
2333
|
-
|
|
1885
|
+
const total = Object.values(scores).reduce((sum, value) => sum + value, 0);
|
|
1886
|
+
if (total !== 0) for (const nodeId of nodeIds) scores[nodeId] /= total;
|
|
1887
|
+
return scores;
|
|
2334
1888
|
}
|
|
2335
1889
|
/**
|
|
2336
|
-
* Returns
|
|
2337
|
-
* Visits neighbors in the order they appear in the adjacency list.
|
|
2338
|
-
*
|
|
2339
|
-
* **O(V + E)** time.
|
|
2340
|
-
*
|
|
2341
|
-
* @example
|
|
2342
|
-
* ```ts
|
|
2343
|
-
* import { createGraph, getPostorder } from '@statelyai/graph';
|
|
2344
|
-
*
|
|
2345
|
-
* const graph = createGraph({
|
|
2346
|
-
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
2347
|
-
* edges: [
|
|
2348
|
-
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
2349
|
-
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
2350
|
-
* ],
|
|
2351
|
-
* initialNodeId: 'a',
|
|
2352
|
-
* });
|
|
1890
|
+
* Returns HITS hub and authority scores for all nodes.
|
|
2353
1891
|
*
|
|
2354
|
-
*
|
|
2355
|
-
* // [nodeC, nodeB, nodeA]
|
|
2356
|
-
* ```
|
|
1892
|
+
* Uses power iteration and L2 normalization per iteration.
|
|
2357
1893
|
*/
|
|
2358
|
-
function
|
|
2359
|
-
const
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
const
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
1894
|
+
function getHITS(graph, options) {
|
|
1895
|
+
const nodeIds = getNodeIds(graph);
|
|
1896
|
+
if (nodeIds.length === 0) return {
|
|
1897
|
+
hubs: {},
|
|
1898
|
+
authorities: {}
|
|
1899
|
+
};
|
|
1900
|
+
const maxIterations = options?.maxIterations ?? 100;
|
|
1901
|
+
const tolerance = options?.tolerance ?? 1e-6;
|
|
1902
|
+
let hubs = Object.fromEntries(nodeIds.map((nodeId) => [nodeId, 1]));
|
|
1903
|
+
let authorities = createEmptyScoreMap(graph);
|
|
1904
|
+
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
1905
|
+
const nextAuthorities = createEmptyScoreMap(graph);
|
|
1906
|
+
for (const nodeId of nodeIds) for (const predecessorId of getIncomingIds(graph, nodeId)) nextAuthorities[nodeId] += hubs[predecessorId];
|
|
1907
|
+
normalizeVector(nextAuthorities);
|
|
1908
|
+
const nextHubs = createEmptyScoreMap(graph);
|
|
1909
|
+
for (const nodeId of nodeIds) for (const neighborId of getNeighborIds(graph, nodeId)) nextHubs[nodeId] += nextAuthorities[neighborId];
|
|
1910
|
+
normalizeVector(nextHubs);
|
|
1911
|
+
const hubDiff = maxDiff(hubs, nextHubs);
|
|
1912
|
+
const authorityDiff = maxDiff(authorities, nextAuthorities);
|
|
1913
|
+
hubs = nextHubs;
|
|
1914
|
+
authorities = nextAuthorities;
|
|
1915
|
+
if (Math.max(hubDiff, authorityDiff) <= tolerance) break;
|
|
2376
1916
|
}
|
|
2377
|
-
return
|
|
1917
|
+
return {
|
|
1918
|
+
hubs,
|
|
1919
|
+
authorities
|
|
1920
|
+
};
|
|
2378
1921
|
}
|
|
2379
1922
|
/**
|
|
2380
|
-
* Returns
|
|
2381
|
-
*
|
|
2382
|
-
* **O(V! · V)** worst-case (exponential).
|
|
2383
|
-
*
|
|
2384
|
-
* @example
|
|
2385
|
-
* ```ts
|
|
2386
|
-
* import { createGraph, getPreorders } from '@statelyai/graph';
|
|
2387
|
-
*
|
|
2388
|
-
* const graph = createGraph({
|
|
2389
|
-
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
2390
|
-
* edges: [
|
|
2391
|
-
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
2392
|
-
* { id: 'ac', sourceId: 'a', targetId: 'c' },
|
|
2393
|
-
* ],
|
|
2394
|
-
* initialNodeId: 'a',
|
|
2395
|
-
* });
|
|
1923
|
+
* Returns eigenvector centrality scores for all nodes.
|
|
2396
1924
|
*
|
|
2397
|
-
*
|
|
2398
|
-
*
|
|
2399
|
-
* ```
|
|
1925
|
+
* Uses power iteration over incoming neighbors for directed graphs and
|
|
1926
|
+
* undirected adjacency for undirected graphs.
|
|
2400
1927
|
*/
|
|
2401
|
-
function
|
|
2402
|
-
|
|
1928
|
+
function getEigenvectorCentrality(graph, options) {
|
|
1929
|
+
const nodeIds = getNodeIds(graph);
|
|
1930
|
+
if (nodeIds.length === 0) return {};
|
|
1931
|
+
const maxIterations = options?.maxIterations ?? 100;
|
|
1932
|
+
const tolerance = options?.tolerance ?? 1e-6;
|
|
1933
|
+
let scores = Object.fromEntries(nodeIds.map((nodeId) => [nodeId, 1]));
|
|
1934
|
+
normalizeVector(scores);
|
|
1935
|
+
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
1936
|
+
const nextScores = createEmptyScoreMap(graph);
|
|
1937
|
+
for (const nodeId of nodeIds) for (const predecessorId of getIncomingIds(graph, nodeId)) nextScores[nodeId] += scores[predecessorId];
|
|
1938
|
+
normalizeVector(nextScores);
|
|
1939
|
+
const diff = maxDiff(scores, nextScores);
|
|
1940
|
+
scores = nextScores;
|
|
1941
|
+
if (diff <= tolerance) break;
|
|
1942
|
+
}
|
|
1943
|
+
return scores;
|
|
2403
1944
|
}
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
*/
|
|
2426
|
-
function getPostorders(graph, opts) {
|
|
2427
|
-
return [...genPostorders(graph, opts)];
|
|
1945
|
+
|
|
1946
|
+
//#endregion
|
|
1947
|
+
//#region src/algorithms/community.ts
|
|
1948
|
+
function getUndirectedNeighbors$1(graph, nodeId) {
|
|
1949
|
+
const idx = getIndex(graph);
|
|
1950
|
+
const neighbors = [];
|
|
1951
|
+
for (const edgeId of idx.outEdges.get(nodeId) ?? []) {
|
|
1952
|
+
const edgeIndex = idx.edgeById.get(edgeId);
|
|
1953
|
+
if (edgeIndex !== void 0) neighbors.push({
|
|
1954
|
+
nodeId: graph.edges[edgeIndex].targetId,
|
|
1955
|
+
edgeId
|
|
1956
|
+
});
|
|
1957
|
+
}
|
|
1958
|
+
for (const edgeId of idx.inEdges.get(nodeId) ?? []) {
|
|
1959
|
+
const edgeIndex = idx.edgeById.get(edgeId);
|
|
1960
|
+
if (edgeIndex !== void 0) neighbors.push({
|
|
1961
|
+
nodeId: graph.edges[edgeIndex].sourceId,
|
|
1962
|
+
edgeId
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1965
|
+
return neighbors;
|
|
2428
1966
|
}
|
|
2429
|
-
|
|
2430
|
-
* Lazily yields all possible preorder (DFS visit-order) sequences.
|
|
2431
|
-
* Different neighbor exploration orders yield different sequences.
|
|
2432
|
-
* Use `getPreorder()` for a single canonical ordering.
|
|
2433
|
-
*
|
|
2434
|
-
* **O(V! · V)** worst-case (exponential).
|
|
2435
|
-
*
|
|
2436
|
-
* @example
|
|
2437
|
-
* ```ts
|
|
2438
|
-
* import { createGraph, genPreorders } from '@statelyai/graph';
|
|
2439
|
-
*
|
|
2440
|
-
* const graph = createGraph({
|
|
2441
|
-
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
2442
|
-
* edges: [
|
|
2443
|
-
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
2444
|
-
* { id: 'ac', sourceId: 'a', targetId: 'c' },
|
|
2445
|
-
* ],
|
|
2446
|
-
* initialNodeId: 'a',
|
|
2447
|
-
* });
|
|
2448
|
-
*
|
|
2449
|
-
* for (const order of genPreorders(graph)) {
|
|
2450
|
-
* console.log(order.map(n => n.id));
|
|
2451
|
-
* // ['a', 'b', 'c'] or ['a', 'c', 'b']
|
|
2452
|
-
* }
|
|
2453
|
-
* ```
|
|
2454
|
-
*/
|
|
2455
|
-
function* genPreorders(graph, opts) {
|
|
1967
|
+
function getUndirectedConnectedComponents(graph) {
|
|
2456
1968
|
const idx = getIndex(graph);
|
|
2457
|
-
const
|
|
2458
|
-
const
|
|
2459
|
-
const
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
const top = dfsStack[dfsStack.length - 1];
|
|
2473
|
-
const unvisited = getNeighborIds(graph, top).filter((id) => !visited.has(id));
|
|
2474
|
-
if (unvisited.length === 0) {
|
|
2475
|
-
dfsStack.pop();
|
|
2476
|
-
continue;
|
|
1969
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1970
|
+
const communities = [];
|
|
1971
|
+
for (const node of graph.nodes) {
|
|
1972
|
+
if (visited.has(node.id)) continue;
|
|
1973
|
+
const community = [];
|
|
1974
|
+
const queue = [node.id];
|
|
1975
|
+
visited.add(node.id);
|
|
1976
|
+
while (queue.length > 0) {
|
|
1977
|
+
const currentId = queue.shift();
|
|
1978
|
+
const nodeIndex = idx.nodeById.get(currentId);
|
|
1979
|
+
if (nodeIndex !== void 0) community.push(graph.nodes[nodeIndex]);
|
|
1980
|
+
for (const neighbor of getUndirectedNeighbors$1(graph, currentId)) {
|
|
1981
|
+
if (visited.has(neighbor.nodeId)) continue;
|
|
1982
|
+
visited.add(neighbor.nodeId);
|
|
1983
|
+
queue.push(neighbor.nodeId);
|
|
2477
1984
|
}
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
1985
|
+
}
|
|
1986
|
+
communities.push(community.sort((a, b) => a.id.localeCompare(b.id)));
|
|
1987
|
+
}
|
|
1988
|
+
return communities.sort((a, b) => a[0].id.localeCompare(b[0].id));
|
|
1989
|
+
}
|
|
1990
|
+
function getNodeMap(graph) {
|
|
1991
|
+
return new Map(graph.nodes.map((node) => [node.id, node]));
|
|
1992
|
+
}
|
|
1993
|
+
function normalizeCommunities(graph, labels) {
|
|
1994
|
+
const nodeMap = getNodeMap(graph);
|
|
1995
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
1996
|
+
for (const [nodeId, label] of Object.entries(labels)) {
|
|
1997
|
+
if (!grouped.has(label)) grouped.set(label, []);
|
|
1998
|
+
const node = nodeMap.get(nodeId);
|
|
1999
|
+
if (node) grouped.get(label).push(node);
|
|
2000
|
+
}
|
|
2001
|
+
return [...grouped.values()].map((community) => community.sort((a, b) => a.id.localeCompare(b.id))).sort((a, b) => a[0].id.localeCompare(b[0].id));
|
|
2002
|
+
}
|
|
2003
|
+
function getEdgeBetweenness(graph) {
|
|
2004
|
+
const scores = Object.fromEntries(graph.edges.map((edge) => [edge.id, 0]));
|
|
2005
|
+
for (const source of graph.nodes) {
|
|
2006
|
+
const stack = [];
|
|
2007
|
+
const predecessors = /* @__PURE__ */ new Map();
|
|
2008
|
+
const sigma = /* @__PURE__ */ new Map();
|
|
2009
|
+
const distance = /* @__PURE__ */ new Map();
|
|
2010
|
+
const queue = [source.id];
|
|
2011
|
+
for (const node of graph.nodes) {
|
|
2012
|
+
predecessors.set(node.id, []);
|
|
2013
|
+
sigma.set(node.id, 0);
|
|
2014
|
+
distance.set(node.id, -1);
|
|
2015
|
+
}
|
|
2016
|
+
sigma.set(source.id, 1);
|
|
2017
|
+
distance.set(source.id, 0);
|
|
2018
|
+
while (queue.length > 0) {
|
|
2019
|
+
const currentId = queue.shift();
|
|
2020
|
+
stack.push(currentId);
|
|
2021
|
+
for (const neighbor of getUndirectedNeighbors$1(graph, currentId)) {
|
|
2022
|
+
if (distance.get(neighbor.nodeId) === -1) {
|
|
2023
|
+
queue.push(neighbor.nodeId);
|
|
2024
|
+
distance.set(neighbor.nodeId, distance.get(currentId) + 1);
|
|
2025
|
+
}
|
|
2026
|
+
if (distance.get(neighbor.nodeId) === distance.get(currentId) + 1) {
|
|
2027
|
+
sigma.set(neighbor.nodeId, sigma.get(neighbor.nodeId) + sigma.get(currentId));
|
|
2028
|
+
predecessors.get(neighbor.nodeId).push({
|
|
2029
|
+
nodeId: currentId,
|
|
2030
|
+
edgeId: neighbor.edgeId
|
|
2031
|
+
});
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
const delta = /* @__PURE__ */ new Map();
|
|
2036
|
+
for (const node of graph.nodes) delta.set(node.id, 0);
|
|
2037
|
+
while (stack.length > 0) {
|
|
2038
|
+
const nodeId = stack.pop();
|
|
2039
|
+
const sigmaNode = sigma.get(nodeId);
|
|
2040
|
+
if (sigmaNode === 0) continue;
|
|
2041
|
+
for (const predecessor of predecessors.get(nodeId)) {
|
|
2042
|
+
const contribution = sigma.get(predecessor.nodeId) / sigmaNode * (1 + delta.get(nodeId));
|
|
2043
|
+
scores[predecessor.edgeId] += contribution;
|
|
2044
|
+
delta.set(predecessor.nodeId, delta.get(predecessor.nodeId) + contribution);
|
|
2488
2045
|
}
|
|
2489
|
-
branched = true;
|
|
2490
|
-
break;
|
|
2491
2046
|
}
|
|
2492
|
-
if (!branched) yield preorder;
|
|
2493
2047
|
}
|
|
2048
|
+
for (const edgeId of Object.keys(scores)) scores[edgeId] /= 2;
|
|
2049
|
+
return scores;
|
|
2050
|
+
}
|
|
2051
|
+
function cloneWithEdges(graph, edges) {
|
|
2052
|
+
return {
|
|
2053
|
+
...graph,
|
|
2054
|
+
nodes: [...graph.nodes],
|
|
2055
|
+
edges
|
|
2056
|
+
};
|
|
2057
|
+
}
|
|
2058
|
+
function toCommunityIds(communities) {
|
|
2059
|
+
return communities.map((community) => new Set(community.map((node) => node.id)));
|
|
2494
2060
|
}
|
|
2495
2061
|
/**
|
|
2496
|
-
*
|
|
2497
|
-
* Different neighbor exploration orders yield different sequences.
|
|
2498
|
-
* Use `getPostorder()` for a single canonical ordering.
|
|
2499
|
-
*
|
|
2500
|
-
* **O(V! · V)** worst-case (exponential).
|
|
2501
|
-
*
|
|
2502
|
-
* @example
|
|
2503
|
-
* ```ts
|
|
2504
|
-
* import { createGraph, genPostorders } from '@statelyai/graph';
|
|
2505
|
-
*
|
|
2506
|
-
* const graph = createGraph({
|
|
2507
|
-
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
2508
|
-
* edges: [
|
|
2509
|
-
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
2510
|
-
* { id: 'ac', sourceId: 'a', targetId: 'c' },
|
|
2511
|
-
* ],
|
|
2512
|
-
* initialNodeId: 'a',
|
|
2513
|
-
* });
|
|
2062
|
+
* Returns label-propagation communities for the graph.
|
|
2514
2063
|
*
|
|
2515
|
-
*
|
|
2516
|
-
*
|
|
2517
|
-
* // ['b', 'c', 'a'] or ['c', 'b', 'a']
|
|
2518
|
-
* }
|
|
2519
|
-
* ```
|
|
2064
|
+
* The implementation is deterministic: ties are broken by lexicographic label
|
|
2065
|
+
* order so test results remain stable.
|
|
2520
2066
|
*/
|
|
2521
|
-
function
|
|
2522
|
-
|
|
2523
|
-
const
|
|
2524
|
-
|
|
2525
|
-
const
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
let branched = false;
|
|
2535
|
-
while (dfsStack.length > 0) {
|
|
2536
|
-
const top = dfsStack[dfsStack.length - 1];
|
|
2537
|
-
const unvisited = getNeighborIds(graph, top).filter((id) => !visited.has(id));
|
|
2538
|
-
if (unvisited.length === 0) {
|
|
2539
|
-
dfsStack.pop();
|
|
2540
|
-
const ni = idx.nodeById.get(top);
|
|
2541
|
-
if (ni !== void 0) postorder = [...postorder, graph.nodes[ni]];
|
|
2542
|
-
continue;
|
|
2067
|
+
function getLabelPropagationCommunities(graph, options) {
|
|
2068
|
+
if (graph.nodes.length === 0) return [];
|
|
2069
|
+
const maxIterations = options?.maxIterations ?? 50;
|
|
2070
|
+
let labels = Object.fromEntries(graph.nodes.map((node) => [node.id, node.id]));
|
|
2071
|
+
const nodeIds = graph.nodes.map((node) => node.id).sort();
|
|
2072
|
+
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
2073
|
+
const nextLabels = { ...labels };
|
|
2074
|
+
let changed = false;
|
|
2075
|
+
for (const nodeId of nodeIds) {
|
|
2076
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2077
|
+
for (const neighbor of getUndirectedNeighbors$1(graph, nodeId)) {
|
|
2078
|
+
const label = labels[neighbor.nodeId];
|
|
2079
|
+
counts.set(label, (counts.get(label) ?? 0) + 1);
|
|
2543
2080
|
}
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
});
|
|
2081
|
+
if (counts.size === 0) continue;
|
|
2082
|
+
const bestLabel = [...counts.entries()].sort((a, b) => {
|
|
2083
|
+
if (b[1] !== a[1]) return b[1] - a[1];
|
|
2084
|
+
return a[0].localeCompare(b[0]);
|
|
2085
|
+
})[0][0];
|
|
2086
|
+
if (bestLabel !== labels[nodeId]) {
|
|
2087
|
+
nextLabels[nodeId] = bestLabel;
|
|
2088
|
+
changed = true;
|
|
2553
2089
|
}
|
|
2554
|
-
branched = true;
|
|
2555
|
-
break;
|
|
2556
2090
|
}
|
|
2557
|
-
|
|
2091
|
+
labels = nextLabels;
|
|
2092
|
+
if (!changed) break;
|
|
2558
2093
|
}
|
|
2094
|
+
return normalizeCommunities(graph, labels);
|
|
2559
2095
|
}
|
|
2560
2096
|
/**
|
|
2561
|
-
*
|
|
2562
|
-
*
|
|
2563
|
-
* from an arbitrary start node in directed graphs).
|
|
2564
|
-
*
|
|
2565
|
-
* **O(E log E)** using either edge sorting (Kruskal) or a min-heap (Prim).
|
|
2566
|
-
*
|
|
2567
|
-
* @example
|
|
2568
|
-
* ```ts
|
|
2569
|
-
* import { createGraph, getMinimumSpanningTree } from '@statelyai/graph';
|
|
2570
|
-
*
|
|
2571
|
-
* const graph = createGraph({
|
|
2572
|
-
* type: 'undirected',
|
|
2573
|
-
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
2574
|
-
* edges: [
|
|
2575
|
-
* { id: 'ab', sourceId: 'a', targetId: 'b', data: { weight: 1 } },
|
|
2576
|
-
* { id: 'bc', sourceId: 'b', targetId: 'c', data: { weight: 2 } },
|
|
2577
|
-
* { id: 'ac', sourceId: 'a', targetId: 'c', data: { weight: 3 } },
|
|
2578
|
-
* ],
|
|
2579
|
-
* });
|
|
2580
|
-
*
|
|
2581
|
-
* const mst = getMinimumSpanningTree(graph, {
|
|
2582
|
-
* getWeight: (e) => e.data.weight,
|
|
2583
|
-
* });
|
|
2584
|
-
* // mst has edges 'ab' and 'bc' (total weight 3)
|
|
2585
|
-
* ```
|
|
2097
|
+
* Lazily yields Girvan-Newman community splits as edge betweenness removes
|
|
2098
|
+
* bridge-like edges from the graph.
|
|
2586
2099
|
*/
|
|
2587
|
-
function
|
|
2588
|
-
|
|
2589
|
-
const
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
edges: mstEdges.map((e) => ({
|
|
2603
|
-
id: e.id,
|
|
2604
|
-
sourceId: e.sourceId,
|
|
2605
|
-
targetId: e.targetId,
|
|
2606
|
-
label: e.label,
|
|
2607
|
-
data: e.data,
|
|
2608
|
-
...e.weight !== void 0 && { weight: e.weight }
|
|
2609
|
-
}))
|
|
2610
|
-
});
|
|
2611
|
-
}
|
|
2612
|
-
function primMST(graph, getWeight) {
|
|
2613
|
-
if (graph.nodes.length === 0) return [];
|
|
2614
|
-
const idx = getIndex(graph);
|
|
2615
|
-
const inMST = /* @__PURE__ */ new Set();
|
|
2616
|
-
const mstEdges = [];
|
|
2617
|
-
const candidates = new MinPriorityQueue((a, b) => a.weight - b.weight);
|
|
2618
|
-
function addEdgesOf(nodeId) {
|
|
2619
|
-
for (const eid of idx.outEdges.get(nodeId) ?? []) {
|
|
2620
|
-
const ai = idx.edgeById.get(eid);
|
|
2621
|
-
if (ai === void 0) continue;
|
|
2622
|
-
const e = graph.edges[ai];
|
|
2623
|
-
if (!inMST.has(e.targetId)) candidates.push({
|
|
2624
|
-
weight: getWeight(e),
|
|
2625
|
-
edge: e
|
|
2626
|
-
});
|
|
2627
|
-
}
|
|
2628
|
-
if (graph.type === "undirected") for (const eid of idx.inEdges.get(nodeId) ?? []) {
|
|
2629
|
-
const ai = idx.edgeById.get(eid);
|
|
2630
|
-
if (ai === void 0) continue;
|
|
2631
|
-
const e = graph.edges[ai];
|
|
2632
|
-
if (!inMST.has(e.sourceId)) candidates.push({
|
|
2633
|
-
weight: getWeight(e),
|
|
2634
|
-
edge: e
|
|
2635
|
-
});
|
|
2100
|
+
function* genGirvanNewmanCommunities(graph, options) {
|
|
2101
|
+
if (graph.nodes.length === 0 || graph.edges.length === 0) return;
|
|
2102
|
+
const maxLevels = options?.maxLevels ?? Number.POSITIVE_INFINITY;
|
|
2103
|
+
let yielded = 0;
|
|
2104
|
+
let edges = [...graph.edges];
|
|
2105
|
+
let previousCount = getUndirectedConnectedComponents(graph).length;
|
|
2106
|
+
while (edges.length > 0 && yielded < maxLevels) {
|
|
2107
|
+
const betweenness = getEdgeBetweenness(cloneWithEdges(graph, edges));
|
|
2108
|
+
const maxScore = Math.max(...Object.values(betweenness));
|
|
2109
|
+
edges = edges.filter((edge) => betweenness[edge.id] < maxScore - 1e-12);
|
|
2110
|
+
const components = getUndirectedConnectedComponents(cloneWithEdges(graph, edges));
|
|
2111
|
+
if (components.length > previousCount) {
|
|
2112
|
+
yield components;
|
|
2113
|
+
yielded++;
|
|
2114
|
+
previousCount = components.length;
|
|
2636
2115
|
}
|
|
2637
2116
|
}
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2117
|
+
}
|
|
2118
|
+
/**
|
|
2119
|
+
* Returns the requested Girvan-Newman split level eagerly.
|
|
2120
|
+
*
|
|
2121
|
+
* `level: 1` returns the first split yielded by `genGirvanNewmanCommunities`.
|
|
2122
|
+
*/
|
|
2123
|
+
function getGirvanNewmanCommunities(graph, options) {
|
|
2124
|
+
if (graph.nodes.length === 0) return [];
|
|
2125
|
+
const targetLevel = options?.level ?? 1;
|
|
2126
|
+
if (targetLevel <= 0) return getUndirectedConnectedComponents(graph);
|
|
2127
|
+
let last = getUndirectedConnectedComponents(graph);
|
|
2128
|
+
let level = 0;
|
|
2129
|
+
for (const partition of genGirvanNewmanCommunities(graph, { maxLevels: targetLevel })) {
|
|
2130
|
+
last = partition;
|
|
2131
|
+
level++;
|
|
2132
|
+
if (level >= targetLevel) break;
|
|
2648
2133
|
}
|
|
2649
|
-
return
|
|
2134
|
+
return last;
|
|
2650
2135
|
}
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2136
|
+
/**
|
|
2137
|
+
* Returns the modularity score for a partition of communities.
|
|
2138
|
+
*
|
|
2139
|
+
* Community algorithms in this module treat the graph as undirected.
|
|
2140
|
+
*/
|
|
2141
|
+
function getModularity(graph, communities) {
|
|
2142
|
+
if (graph.edges.length === 0 || communities.length === 0) return 0;
|
|
2143
|
+
const nodeIds = graph.nodes.map((node) => node.id);
|
|
2144
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
2145
|
+
const degree = Object.fromEntries(nodeIds.map((nodeId) => [nodeId, 0]));
|
|
2146
|
+
for (const nodeId of nodeIds) adjacency.set(nodeId, /* @__PURE__ */ new Map());
|
|
2147
|
+
for (const edge of graph.edges) {
|
|
2148
|
+
adjacency.get(edge.sourceId).set(edge.targetId, (adjacency.get(edge.sourceId).get(edge.targetId) ?? 0) + 1);
|
|
2149
|
+
adjacency.get(edge.targetId).set(edge.sourceId, (adjacency.get(edge.targetId).get(edge.sourceId) ?? 0) + 1);
|
|
2150
|
+
degree[edge.sourceId]++;
|
|
2151
|
+
degree[edge.targetId]++;
|
|
2152
|
+
}
|
|
2153
|
+
const m2 = graph.edges.length * 2;
|
|
2154
|
+
let modularity = 0;
|
|
2155
|
+
for (const community of toCommunityIds(communities)) {
|
|
2156
|
+
const ids = [...community];
|
|
2157
|
+
for (const i of ids) for (const j of ids) {
|
|
2158
|
+
const aij = adjacency.get(i).get(j) ?? 0;
|
|
2159
|
+
modularity += aij - degree[i] * degree[j] / m2;
|
|
2671
2160
|
}
|
|
2672
|
-
return true;
|
|
2673
2161
|
}
|
|
2674
|
-
|
|
2675
|
-
for (const e of sorted) if (union(e.sourceId, e.targetId)) mstEdges.push(e);
|
|
2676
|
-
return mstEdges;
|
|
2162
|
+
return modularity / m2;
|
|
2677
2163
|
}
|
|
2678
2164
|
/**
|
|
2679
|
-
* Returns
|
|
2680
|
-
*
|
|
2681
|
-
* Algorithm 'floyd-warshall': classic dynamic programming.
|
|
2682
|
-
*
|
|
2683
|
-
* **O(V · (V + E) log V)** (Dijkstra) or **O(V³)** (Floyd-Warshall).
|
|
2684
|
-
*
|
|
2685
|
-
* @example
|
|
2686
|
-
* ```ts
|
|
2687
|
-
* import { createGraph, getAllPairsShortestPaths } from '@statelyai/graph';
|
|
2688
|
-
*
|
|
2689
|
-
* const graph = createGraph({
|
|
2690
|
-
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
2691
|
-
* edges: [
|
|
2692
|
-
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
2693
|
-
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
2694
|
-
* ],
|
|
2695
|
-
* });
|
|
2696
|
-
*
|
|
2697
|
-
* const allPaths = getAllPairsShortestPaths(graph);
|
|
2698
|
-
* // paths for every reachable (source, target) pair
|
|
2699
|
-
* ```
|
|
2165
|
+
* Returns communities found by greedily merging partitions that improve
|
|
2166
|
+
* modularity the most at each step.
|
|
2700
2167
|
*/
|
|
2701
|
-
function
|
|
2702
|
-
if (
|
|
2703
|
-
|
|
2168
|
+
function getGreedyModularityCommunities(graph) {
|
|
2169
|
+
if (graph.nodes.length === 0) return [];
|
|
2170
|
+
let communities = graph.nodes.map((node) => [node]);
|
|
2171
|
+
let currentScore = getModularity(graph, communities);
|
|
2172
|
+
while (communities.length > 1) {
|
|
2173
|
+
let bestScore = currentScore;
|
|
2174
|
+
let bestMerge;
|
|
2175
|
+
for (let i = 0; i < communities.length; i++) for (let j = i + 1; j < communities.length; j++) {
|
|
2176
|
+
const merged = communities.filter((_, index) => index !== i && index !== j);
|
|
2177
|
+
merged.push([...communities[i], ...communities[j]].sort((a, b) => a.id.localeCompare(b.id)));
|
|
2178
|
+
const score = getModularity(graph, merged);
|
|
2179
|
+
if (score > bestScore + 1e-12) {
|
|
2180
|
+
bestScore = score;
|
|
2181
|
+
bestMerge = merged;
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
if (!bestMerge) break;
|
|
2185
|
+
communities = bestMerge.sort((a, b) => a[0].id.localeCompare(b[0].id));
|
|
2186
|
+
currentScore = bestScore;
|
|
2187
|
+
}
|
|
2188
|
+
return communities;
|
|
2704
2189
|
}
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2190
|
+
|
|
2191
|
+
//#endregion
|
|
2192
|
+
//#region src/algorithms/connectivity.ts
|
|
2193
|
+
function getUndirectedNeighbors(graph, nodeId) {
|
|
2194
|
+
const idx = getIndex(graph);
|
|
2195
|
+
const neighbors = [];
|
|
2196
|
+
for (const edgeId of idx.outEdges.get(nodeId) ?? []) {
|
|
2197
|
+
const edgeIndex = idx.edgeById.get(edgeId);
|
|
2198
|
+
if (edgeIndex !== void 0) neighbors.push({
|
|
2199
|
+
nodeId: graph.edges[edgeIndex].targetId,
|
|
2200
|
+
edgeId
|
|
2201
|
+
});
|
|
2202
|
+
}
|
|
2203
|
+
for (const edgeId of idx.inEdges.get(nodeId) ?? []) {
|
|
2204
|
+
const edgeIndex = idx.edgeById.get(edgeId);
|
|
2205
|
+
if (edgeIndex !== void 0) neighbors.push({
|
|
2206
|
+
nodeId: graph.edges[edgeIndex].sourceId,
|
|
2207
|
+
edgeId
|
|
2711
2208
|
});
|
|
2712
|
-
results.push(...paths);
|
|
2713
2209
|
}
|
|
2714
|
-
return
|
|
2210
|
+
return neighbors;
|
|
2715
2211
|
}
|
|
2716
|
-
function
|
|
2717
|
-
const
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
const dist = Array.from({ length: n }, () => Array(n).fill(INF));
|
|
2725
|
-
const prev = Array.from({ length: n }, () => Array.from({ length: n }, () => []));
|
|
2726
|
-
for (let i = 0; i < n; i++) dist[i][i] = 0;
|
|
2727
|
-
for (const e of graph.edges) {
|
|
2728
|
-
const u = idxOf.get(e.sourceId);
|
|
2729
|
-
const v = idxOf.get(e.targetId);
|
|
2730
|
-
const weight = w(e);
|
|
2731
|
-
if (weight < dist[u][v]) {
|
|
2732
|
-
dist[u][v] = weight;
|
|
2733
|
-
prev[u][v] = [{
|
|
2734
|
-
from: u,
|
|
2735
|
-
edge: e
|
|
2736
|
-
}];
|
|
2737
|
-
} else if (weight === dist[u][v] && weight < INF) prev[u][v].push({
|
|
2738
|
-
from: u,
|
|
2739
|
-
edge: e
|
|
2740
|
-
});
|
|
2741
|
-
if (graph.type === "undirected") {
|
|
2742
|
-
if (weight < dist[v][u]) {
|
|
2743
|
-
dist[v][u] = weight;
|
|
2744
|
-
prev[v][u] = [{
|
|
2745
|
-
from: v,
|
|
2746
|
-
edge: e
|
|
2747
|
-
}];
|
|
2748
|
-
} else if (weight === dist[v][u] && weight < INF) prev[v][u].push({
|
|
2749
|
-
from: v,
|
|
2750
|
-
edge: e
|
|
2751
|
-
});
|
|
2212
|
+
function popComponentUntil(state, stopEdgeId) {
|
|
2213
|
+
const nodeIds = /* @__PURE__ */ new Set();
|
|
2214
|
+
while (state.edgeStack.length > 0) {
|
|
2215
|
+
const edgeId = state.edgeStack.pop();
|
|
2216
|
+
const edge = state.edgeById.get(edgeId);
|
|
2217
|
+
if (edge) {
|
|
2218
|
+
nodeIds.add(edge.sourceId);
|
|
2219
|
+
nodeIds.add(edge.targetId);
|
|
2752
2220
|
}
|
|
2221
|
+
if (edgeId === stopEdgeId) break;
|
|
2753
2222
|
}
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2223
|
+
if (nodeIds.size > 0) state.components.push(nodeIds);
|
|
2224
|
+
}
|
|
2225
|
+
function finalizeRemainingComponent(state) {
|
|
2226
|
+
if (state.edgeStack.length === 0) return;
|
|
2227
|
+
const nodeIds = /* @__PURE__ */ new Set();
|
|
2228
|
+
while (state.edgeStack.length > 0) {
|
|
2229
|
+
const edge = state.edgeById.get(state.edgeStack.pop());
|
|
2230
|
+
if (edge) {
|
|
2231
|
+
nodeIds.add(edge.sourceId);
|
|
2232
|
+
nodeIds.add(edge.targetId);
|
|
2762
2233
|
}
|
|
2763
2234
|
}
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2235
|
+
if (nodeIds.size > 0) state.components.push(nodeIds);
|
|
2236
|
+
}
|
|
2237
|
+
function traverseConnectivity(graph, nodeId, parentEdgeId, state) {
|
|
2238
|
+
state.time += 1;
|
|
2239
|
+
state.disc.set(nodeId, state.time);
|
|
2240
|
+
state.low.set(nodeId, state.time);
|
|
2241
|
+
let childCount = 0;
|
|
2242
|
+
for (const neighbor of getUndirectedNeighbors(graph, nodeId)) {
|
|
2243
|
+
if (neighbor.edgeId === parentEdgeId) continue;
|
|
2244
|
+
if (!state.disc.has(neighbor.nodeId)) {
|
|
2245
|
+
childCount += 1;
|
|
2246
|
+
state.edgeStack.push(neighbor.edgeId);
|
|
2247
|
+
traverseConnectivity(graph, neighbor.nodeId, neighbor.edgeId, state);
|
|
2248
|
+
state.low.set(nodeId, Math.min(state.low.get(nodeId), state.low.get(neighbor.nodeId)));
|
|
2249
|
+
if (state.low.get(neighbor.nodeId) > state.disc.get(nodeId)) state.bridges.add(neighbor.edgeId);
|
|
2250
|
+
if (parentEdgeId !== null && state.low.get(neighbor.nodeId) >= state.disc.get(nodeId)) {
|
|
2251
|
+
state.articulationPoints.add(nodeId);
|
|
2252
|
+
popComponentUntil(state, neighbor.edgeId);
|
|
2253
|
+
}
|
|
2254
|
+
} else if (state.disc.get(neighbor.nodeId) < state.disc.get(nodeId)) {
|
|
2255
|
+
state.edgeStack.push(neighbor.edgeId);
|
|
2256
|
+
state.low.set(nodeId, Math.min(state.low.get(nodeId), state.disc.get(neighbor.nodeId)));
|
|
2773
2257
|
}
|
|
2774
2258
|
}
|
|
2775
|
-
|
|
2259
|
+
if (parentEdgeId === null && childCount > 1) state.articulationPoints.add(nodeId);
|
|
2776
2260
|
}
|
|
2777
|
-
function
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
edge,
|
|
2794
|
-
node: targetNode
|
|
2795
|
-
}]
|
|
2796
|
-
});
|
|
2261
|
+
function analyzeConnectivity(graph) {
|
|
2262
|
+
const state = {
|
|
2263
|
+
time: 0,
|
|
2264
|
+
disc: /* @__PURE__ */ new Map(),
|
|
2265
|
+
low: /* @__PURE__ */ new Map(),
|
|
2266
|
+
edgeStack: [],
|
|
2267
|
+
bridges: /* @__PURE__ */ new Set(),
|
|
2268
|
+
articulationPoints: /* @__PURE__ */ new Set(),
|
|
2269
|
+
components: [],
|
|
2270
|
+
nodeById: new Map(graph.nodes.map((node) => [node.id, node])),
|
|
2271
|
+
edgeById: new Map(graph.edges.map((edge) => [edge.id, edge]))
|
|
2272
|
+
};
|
|
2273
|
+
for (const node of graph.nodes) {
|
|
2274
|
+
if (state.disc.has(node.id)) continue;
|
|
2275
|
+
traverseConnectivity(graph, node.id, null, state);
|
|
2276
|
+
finalizeRemainingComponent(state);
|
|
2797
2277
|
}
|
|
2798
|
-
return
|
|
2278
|
+
return state;
|
|
2799
2279
|
}
|
|
2800
2280
|
/**
|
|
2801
|
-
* Returns
|
|
2802
|
-
* More efficient than Dijkstra when a good heuristic is available.
|
|
2803
|
-
*
|
|
2804
|
-
* **O((V + E) log V)** time with a good heuristic; degrades to Dijkstra
|
|
2805
|
-
* with `heuristic: () => 0`.
|
|
2281
|
+
* Returns bridge edges whose removal disconnects the graph.
|
|
2806
2282
|
*
|
|
2807
|
-
*
|
|
2808
|
-
|
|
2809
|
-
|
|
2283
|
+
* Connectivity algorithms in this module treat the graph as undirected.
|
|
2284
|
+
*/
|
|
2285
|
+
function getBridges(graph) {
|
|
2286
|
+
if (graph.edges.length === 0) return [];
|
|
2287
|
+
const state = analyzeConnectivity(graph);
|
|
2288
|
+
return [...state.bridges].map((edgeId) => state.edgeById.get(edgeId)).sort((a, b) => a.id.localeCompare(b.id));
|
|
2289
|
+
}
|
|
2290
|
+
/**
|
|
2291
|
+
* Returns articulation points (cut vertices) for the graph.
|
|
2810
2292
|
*
|
|
2811
|
-
*
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
*
|
|
2820
|
-
* { id: 'ac', sourceId: 'a', targetId: 'c', weight: 3 },
|
|
2821
|
-
* ],
|
|
2822
|
-
* });
|
|
2293
|
+
* Connectivity algorithms in this module treat the graph as undirected.
|
|
2294
|
+
*/
|
|
2295
|
+
function getArticulationPoints(graph) {
|
|
2296
|
+
if (graph.nodes.length === 0) return [];
|
|
2297
|
+
const state = analyzeConnectivity(graph);
|
|
2298
|
+
return [...state.articulationPoints].map((nodeId) => state.nodeById.get(nodeId)).sort((a, b) => a.id.localeCompare(b.id));
|
|
2299
|
+
}
|
|
2300
|
+
/**
|
|
2301
|
+
* Returns biconnected components as arrays of nodes.
|
|
2823
2302
|
*
|
|
2824
|
-
*
|
|
2825
|
-
* from: 'a',
|
|
2826
|
-
* to: 'c',
|
|
2827
|
-
* heuristic: (nodeId) => {
|
|
2828
|
-
* const node = graph.nodes.find(n => n.id === nodeId)!;
|
|
2829
|
-
* const target = graph.nodes.find(n => n.id === 'c')!;
|
|
2830
|
-
* return Math.abs(node.x! - target.x!) + Math.abs(node.y! - target.y!);
|
|
2831
|
-
* },
|
|
2832
|
-
* });
|
|
2833
|
-
* // path: a -> b -> c (weight 2, cheaper than direct a -> c)
|
|
2834
|
-
* ```
|
|
2303
|
+
* Articulation points may appear in multiple returned components.
|
|
2835
2304
|
*/
|
|
2836
|
-
function
|
|
2305
|
+
function getBiconnectedComponents(graph) {
|
|
2306
|
+
if (graph.edges.length === 0) return [];
|
|
2307
|
+
const state = analyzeConnectivity(graph);
|
|
2308
|
+
return state.components.map((component) => [...component].map((nodeId) => state.nodeById.get(nodeId)).sort((a, b) => a.id.localeCompare(b.id))).sort((a, b) => a[0].id.localeCompare(b[0].id));
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
//#endregion
|
|
2312
|
+
//#region src/algorithms/isomorphism.ts
|
|
2313
|
+
function getDegreeSignature(graph, nodeId) {
|
|
2837
2314
|
const idx = getIndex(graph);
|
|
2838
|
-
const
|
|
2839
|
-
const
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
const
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
});
|
|
2856
|
-
while (openSet.size > 0) {
|
|
2857
|
-
const { id: currentId } = openSet.pop();
|
|
2858
|
-
if (closedSet.has(currentId)) continue;
|
|
2859
|
-
if (currentId === targetId) {
|
|
2860
|
-
const steps = [];
|
|
2861
|
-
let cur = targetId;
|
|
2862
|
-
while (cur !== sourceId) {
|
|
2863
|
-
const prev = cameFrom.get(cur);
|
|
2864
|
-
const ni = idx.nodeById.get(cur);
|
|
2865
|
-
steps.unshift({
|
|
2866
|
-
edge: prev.edge,
|
|
2867
|
-
node: graph.nodes[ni]
|
|
2868
|
-
});
|
|
2869
|
-
cur = prev.from;
|
|
2870
|
-
}
|
|
2871
|
-
return {
|
|
2872
|
-
source: graph.nodes[sourceNi],
|
|
2873
|
-
steps
|
|
2874
|
-
};
|
|
2875
|
-
}
|
|
2876
|
-
closedSet.add(currentId);
|
|
2877
|
-
for (const { neighborId, edge } of getNeighborEdges(graph, currentId)) {
|
|
2878
|
-
if (closedSet.has(neighborId)) continue;
|
|
2879
|
-
const tentativeG = (gScore.get(currentId) ?? Infinity) + getWeight(edge);
|
|
2880
|
-
if (tentativeG < (gScore.get(neighborId) ?? Infinity)) {
|
|
2881
|
-
cameFrom.set(neighborId, {
|
|
2882
|
-
from: currentId,
|
|
2883
|
-
edge
|
|
2884
|
-
});
|
|
2885
|
-
gScore.set(neighborId, tentativeG);
|
|
2886
|
-
openSet.push({
|
|
2887
|
-
id: neighborId,
|
|
2888
|
-
f: tentativeG + heuristic(neighborId)
|
|
2889
|
-
});
|
|
2890
|
-
}
|
|
2891
|
-
}
|
|
2315
|
+
const outDegree = idx.outEdges.get(nodeId)?.length ?? 0;
|
|
2316
|
+
const inDegree = idx.inEdges.get(nodeId)?.length ?? 0;
|
|
2317
|
+
if (graph.type === "undirected") return `u:${new Set([...idx.outEdges.get(nodeId) ?? [], ...idx.inEdges.get(nodeId) ?? []]).size}`;
|
|
2318
|
+
return `d:${inDegree}:${outDegree}`;
|
|
2319
|
+
}
|
|
2320
|
+
function getEdgesBetween(graph, sourceId, targetId) {
|
|
2321
|
+
if (graph.type === "undirected") return graph.edges.filter((edge) => edge.sourceId === sourceId && edge.targetId === targetId || edge.sourceId === targetId && edge.targetId === sourceId);
|
|
2322
|
+
return graph.edges.filter((edge) => edge.sourceId === sourceId && edge.targetId === targetId);
|
|
2323
|
+
}
|
|
2324
|
+
function edgesAreCompatible(edgesA, edgesB, edgeMatch) {
|
|
2325
|
+
if (edgesA.length !== edgesB.length) return false;
|
|
2326
|
+
if (!edgeMatch || edgesA.length === 0) return true;
|
|
2327
|
+
const remaining = [...edgesB];
|
|
2328
|
+
for (const edgeA of edgesA) {
|
|
2329
|
+
const matchIndex = remaining.findIndex((edgeB) => edgeMatch(edgeA, edgeB));
|
|
2330
|
+
if (matchIndex === -1) return false;
|
|
2331
|
+
remaining.splice(matchIndex, 1);
|
|
2892
2332
|
}
|
|
2333
|
+
return true;
|
|
2893
2334
|
}
|
|
2894
2335
|
/**
|
|
2895
|
-
*
|
|
2896
|
-
* the source of the tail path (the overlap node).
|
|
2897
|
-
*
|
|
2898
|
-
* Steps are concatenated: head.steps ++ tail.steps (tail already starts
|
|
2899
|
-
* from the overlap node, so no slicing is needed).
|
|
2900
|
-
*
|
|
2901
|
-
* @example
|
|
2902
|
-
* ```ts
|
|
2903
|
-
* import { createGraph, getShortestPath, joinPaths } from '@statelyai/graph';
|
|
2904
|
-
*
|
|
2905
|
-
* const graph = createGraph({
|
|
2906
|
-
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
2907
|
-
* edges: [
|
|
2908
|
-
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
2909
|
-
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
2910
|
-
* ],
|
|
2911
|
-
* initialNodeId: 'a',
|
|
2912
|
-
* });
|
|
2336
|
+
* Returns whether two graphs are structurally isomorphic.
|
|
2913
2337
|
*
|
|
2914
|
-
*
|
|
2915
|
-
*
|
|
2916
|
-
* const ac = joinPaths(ab, bc);
|
|
2917
|
-
* // ac: a -> b -> c
|
|
2918
|
-
* ```
|
|
2338
|
+
* Optional `nodeMatch` and `edgeMatch` predicates can refine the match using
|
|
2339
|
+
* node and edge payloads.
|
|
2919
2340
|
*/
|
|
2920
|
-
function
|
|
2921
|
-
|
|
2922
|
-
if (
|
|
2923
|
-
return
|
|
2924
|
-
|
|
2925
|
-
|
|
2341
|
+
function isIsomorphic(graphA, graphB, options) {
|
|
2342
|
+
if (graphA.type !== graphB.type) return false;
|
|
2343
|
+
if (graphA.nodes.length !== graphB.nodes.length) return false;
|
|
2344
|
+
if (graphA.edges.length !== graphB.edges.length) return false;
|
|
2345
|
+
const nodeMatch = options?.nodeMatch;
|
|
2346
|
+
const edgeMatch = options?.edgeMatch;
|
|
2347
|
+
const nodesA = [...graphA.nodes].sort((a, b) => {
|
|
2348
|
+
const sigDiff = getDegreeSignature(graphA, b.id).localeCompare(getDegreeSignature(graphA, a.id));
|
|
2349
|
+
if (sigDiff !== 0) return sigDiff;
|
|
2350
|
+
return a.id.localeCompare(b.id);
|
|
2351
|
+
});
|
|
2352
|
+
const nodesB = [...graphB.nodes];
|
|
2353
|
+
const signaturesA = nodesA.map((node) => getDegreeSignature(graphA, node.id)).sort();
|
|
2354
|
+
const signaturesB = nodesB.map((node) => getDegreeSignature(graphB, node.id)).sort();
|
|
2355
|
+
if (signaturesA.join("|") !== signaturesB.join("|")) return false;
|
|
2356
|
+
const mapping = /* @__PURE__ */ new Map();
|
|
2357
|
+
const usedB = /* @__PURE__ */ new Set();
|
|
2358
|
+
const backtrack = (index) => {
|
|
2359
|
+
if (index >= nodesA.length) return true;
|
|
2360
|
+
const nodeA = nodesA[index];
|
|
2361
|
+
const signatureA = getDegreeSignature(graphA, nodeA.id);
|
|
2362
|
+
for (const nodeB of nodesB) {
|
|
2363
|
+
if (usedB.has(nodeB.id)) continue;
|
|
2364
|
+
if (getDegreeSignature(graphB, nodeB.id) !== signatureA) continue;
|
|
2365
|
+
if (nodeMatch && !nodeMatch(nodeA, nodeB)) continue;
|
|
2366
|
+
let compatible = true;
|
|
2367
|
+
for (const [mappedAId, mappedBId] of mapping.entries()) {
|
|
2368
|
+
if (!edgesAreCompatible(getEdgesBetween(graphA, nodeA.id, mappedAId), getEdgesBetween(graphB, nodeB.id, mappedBId), edgeMatch)) {
|
|
2369
|
+
compatible = false;
|
|
2370
|
+
break;
|
|
2371
|
+
}
|
|
2372
|
+
if (!edgesAreCompatible(getEdgesBetween(graphA, mappedAId, nodeA.id), getEdgesBetween(graphB, mappedBId, nodeB.id), edgeMatch)) {
|
|
2373
|
+
compatible = false;
|
|
2374
|
+
break;
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
if (!compatible) continue;
|
|
2378
|
+
mapping.set(nodeA.id, nodeB.id);
|
|
2379
|
+
usedB.add(nodeB.id);
|
|
2380
|
+
if (backtrack(index + 1)) return true;
|
|
2381
|
+
mapping.delete(nodeA.id);
|
|
2382
|
+
usedB.delete(nodeB.id);
|
|
2383
|
+
}
|
|
2384
|
+
return false;
|
|
2926
2385
|
};
|
|
2386
|
+
return backtrack(0);
|
|
2927
2387
|
}
|
|
2928
2388
|
|
|
2929
2389
|
//#endregion
|
|
2930
|
-
export { createGraphPort as $,
|
|
2390
|
+
export { createGraphPort as $, isAcyclic as A, getShortestPaths as B, getPreorder as C, getConnectedComponents as D, dfs as E, genSimplePaths as F, GraphInstance as G, getSimplePaths as H, getAStarPath as I, addNode as J, addEdge as K, getAllPairsShortestPaths as L, isTree as M, genCycles as N, getTopologicalSort as O, genShortestPaths as P, createGraphNode as Q, getCycles as R, getPostorders as S, bfs as T, getStronglyConnectedComponents as U, getSimplePath as V, joinPaths as W, createGraphEdge as X, createGraph as Y, createGraphFromTransition as Z, getPageRank as _, genGirvanNewmanCommunities as a, getNode as at, genPreorders as b, getLabelPropagationCommunities as c, updateEdge as ct, getClosenessCentrality as d, createVisualGraph as et, getDegreeCentrality as f, getOutDegreeCentrality as g, getInDegreeCentrality as h, getBridges as i, getEdge as it, isConnected as j, hasPath as k, getModularity as l, updateEntities as lt, getHITS as m, getArticulationPoints as n, deleteEntities as nt, getGirvanNewmanCommunities as o, hasEdge as ot, getEigenvectorCentrality as p, addEntities as q, getBiconnectedComponents as r, deleteNode as rt, getGreedyModularityCommunities as s, hasNode as st, isIsomorphic as t, deleteEdge as tt, getBetweennessCentrality as u, updateNode as ut, getMinimumSpanningTree as v, getPreorders as w, getPostorder as x, genPostorders as y, getShortestPath as z };
|