@statelyai/graph 0.10.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-C-S7u40k.mjs → algorithms-BHHg7lGq.mjs} +1509 -2132
- 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 +2 -2
- package/dist/index.mjs +3 -3
- package/dist/queries.d.mts +1 -1
- package/dist/queries.mjs +1 -1
- package/dist/schemas.d.mts +50 -1
- package/dist/schemas.mjs +21 -2
- 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-DdjFO-ft.d.mts +0 -787
- /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
- /package/dist/{types-F3j-sr2X.d.mts → types-CnZ01raw.d.mts} +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
|
/**
|
|
@@ -650,2364 +650,1741 @@ 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
|
-
|
|
977
|
+
yield* dfsCollect(sourceId);
|
|
975
978
|
}
|
|
976
|
-
function
|
|
977
|
-
|
|
979
|
+
function getSimplePath(graph, opts) {
|
|
980
|
+
for (const path of genSimplePaths(graph, opts)) return path;
|
|
978
981
|
}
|
|
979
|
-
function
|
|
980
|
-
const
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
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)));
|
|
1004
|
+
}
|
|
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);
|
|
1015
|
+
}
|
|
986
1016
|
}
|
|
987
|
-
|
|
1017
|
+
for (const node of graph.nodes) if (!nodeIndex.has(node.id)) strongconnect(node.id);
|
|
1018
|
+
return result;
|
|
988
1019
|
}
|
|
989
|
-
function
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1020
|
+
function getCycles(graph) {
|
|
1021
|
+
return [...genCycles(graph)];
|
|
1022
|
+
}
|
|
1023
|
+
function* genCycles(graph) {
|
|
1024
|
+
if (graph.type === "undirected") yield* genCyclesUndirected(graph);
|
|
1025
|
+
else yield* genCyclesDirected(graph);
|
|
1026
|
+
}
|
|
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]
|
|
1017
1057
|
});
|
|
1058
|
+
dfsFind(neighborId);
|
|
1059
|
+
steps.pop();
|
|
1018
1060
|
}
|
|
1019
1061
|
}
|
|
1062
|
+
visited.delete(currentId);
|
|
1020
1063
|
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1064
|
+
dfsFind(startId);
|
|
1065
|
+
yield* found;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
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
|
+
}
|
|
1031
1108
|
}
|
|
1109
|
+
visited.delete(currentId);
|
|
1032
1110
|
}
|
|
1111
|
+
dfsFind(startId, null);
|
|
1112
|
+
yield* found;
|
|
1033
1113
|
}
|
|
1034
|
-
for (const edgeId of Object.keys(scores)) scores[edgeId] /= 2;
|
|
1035
|
-
return scores;
|
|
1036
1114
|
}
|
|
1037
|
-
function
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
};
|
|
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);
|
|
1043
1120
|
}
|
|
1044
|
-
function
|
|
1045
|
-
|
|
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;
|
|
1046
1129
|
}
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
const
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
}
|
|
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);
|
|
1141
|
+
const nodeIds = graph.nodes.map((node) => node.id);
|
|
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;
|
|
1149
|
+
for (const edge of graph.edges) {
|
|
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
|
+
});
|
|
1076
1174
|
}
|
|
1077
|
-
labels = nextLabels;
|
|
1078
|
-
if (!changed) break;
|
|
1079
1175
|
}
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
let
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
const
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
yield components;
|
|
1099
|
-
yielded++;
|
|
1100
|
-
previousCount = components.length;
|
|
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 });
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
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));
|
|
1101
1194
|
}
|
|
1102
1195
|
}
|
|
1196
|
+
return results;
|
|
1103
1197
|
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
if (
|
|
1111
|
-
const
|
|
1112
|
-
if (
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
for (const
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
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
|
+
}]
|
|
1217
|
+
});
|
|
1119
1218
|
}
|
|
1120
|
-
return
|
|
1219
|
+
return results;
|
|
1121
1220
|
}
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
if (
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
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
|
+
};
|
|
1146
1260
|
}
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
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;
|
|
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
|
+
});
|
|
1168
1275
|
}
|
|
1169
1276
|
}
|
|
1170
|
-
if (!bestMerge) break;
|
|
1171
|
-
communities = bestMerge.sort((a, b) => a[0].id.localeCompare(b[0].id));
|
|
1172
|
-
currentScore = bestScore;
|
|
1173
1277
|
}
|
|
1174
|
-
|
|
1278
|
+
}
|
|
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]
|
|
1285
|
+
};
|
|
1175
1286
|
}
|
|
1176
1287
|
|
|
1177
1288
|
//#endregion
|
|
1178
|
-
//#region src/algorithms/
|
|
1179
|
-
function
|
|
1289
|
+
//#region src/algorithms/traversal.ts
|
|
1290
|
+
function* bfs(graph, startId) {
|
|
1180
1291
|
const idx = getIndex(graph);
|
|
1181
|
-
const
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
nodeId: graph.edges[edgeIndex].sourceId,
|
|
1193
|
-
edgeId
|
|
1194
|
-
});
|
|
1195
|
-
}
|
|
1196
|
-
return neighbors;
|
|
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);
|
|
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);
|
|
1206
1303
|
}
|
|
1207
|
-
if (edgeId === stopEdgeId) break;
|
|
1208
1304
|
}
|
|
1209
|
-
if (nodeIds.size > 0) state.components.push(nodeIds);
|
|
1210
1305
|
}
|
|
1211
|
-
function
|
|
1212
|
-
|
|
1213
|
-
const
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
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);
|
|
1220
1318
|
}
|
|
1221
|
-
if (nodeIds.size > 0) state.components.push(nodeIds);
|
|
1222
1319
|
}
|
|
1223
|
-
function
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
if (
|
|
1236
|
-
if (parentEdgeId !== null && state.low.get(neighbor.nodeId) >= state.disc.get(nodeId)) {
|
|
1237
|
-
state.articulationPoints.add(nodeId);
|
|
1238
|
-
popComponentUntil(state, neighbor.edgeId);
|
|
1239
|
-
}
|
|
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)));
|
|
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;
|
|
1243
1333
|
}
|
|
1244
|
-
|
|
1245
|
-
|
|
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;
|
|
1338
|
+
return true;
|
|
1246
1339
|
}
|
|
1247
|
-
function
|
|
1248
|
-
const
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1340
|
+
function isAcyclicUndirected(graph) {
|
|
1341
|
+
const idx = getIndex(graph);
|
|
1342
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1343
|
+
const hasCycle = (id, parentId) => {
|
|
1344
|
+
visited.add(id);
|
|
1345
|
+
for (const eid of idx.outEdges.get(id) ?? []) {
|
|
1346
|
+
const ai = idx.edgeById.get(eid);
|
|
1347
|
+
if (ai === void 0) continue;
|
|
1348
|
+
const neighborId = graph.edges[ai].targetId;
|
|
1349
|
+
if (!visited.has(neighborId)) {
|
|
1350
|
+
if (hasCycle(neighborId, id)) return true;
|
|
1351
|
+
} else if (neighborId !== parentId) return true;
|
|
1352
|
+
}
|
|
1353
|
+
for (const eid of idx.inEdges.get(id) ?? []) {
|
|
1354
|
+
const ai = idx.edgeById.get(eid);
|
|
1355
|
+
if (ai === void 0) continue;
|
|
1356
|
+
const neighborId = graph.edges[ai].sourceId;
|
|
1357
|
+
if (!visited.has(neighborId)) {
|
|
1358
|
+
if (hasCycle(neighborId, id)) return true;
|
|
1359
|
+
} else if (neighborId !== parentId) return true;
|
|
1360
|
+
}
|
|
1361
|
+
return false;
|
|
1258
1362
|
};
|
|
1259
|
-
for (const node of graph.nodes)
|
|
1260
|
-
|
|
1261
|
-
traverseConnectivity(graph, node.id, null, state);
|
|
1262
|
-
finalizeRemainingComponent(state);
|
|
1263
|
-
}
|
|
1264
|
-
return state;
|
|
1363
|
+
for (const node of graph.nodes) if (!visited.has(node.id) && hasCycle(node.id, null)) return false;
|
|
1364
|
+
return true;
|
|
1265
1365
|
}
|
|
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
|
-
}
|
|
1296
|
-
|
|
1297
|
-
//#endregion
|
|
1298
|
-
//#region src/algorithms/isomorphism.ts
|
|
1299
|
-
function getDegreeSignature(graph, nodeId) {
|
|
1366
|
+
function getConnectedComponents(graph) {
|
|
1300
1367
|
const idx = getIndex(graph);
|
|
1301
|
-
const
|
|
1302
|
-
const
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
return true;
|
|
1320
|
-
}
|
|
1321
|
-
/**
|
|
1322
|
-
* Returns whether two graphs are structurally isomorphic.
|
|
1323
|
-
*
|
|
1324
|
-
* Optional `nodeMatch` and `edgeMatch` predicates can refine the match using
|
|
1325
|
-
* node and edge payloads.
|
|
1326
|
-
*/
|
|
1327
|
-
function isIsomorphic(graphA, graphB, options) {
|
|
1328
|
-
if (graphA.type !== graphB.type) return false;
|
|
1329
|
-
if (graphA.nodes.length !== graphB.nodes.length) return false;
|
|
1330
|
-
if (graphA.edges.length !== graphB.edges.length) return false;
|
|
1331
|
-
const nodeMatch = options?.nodeMatch;
|
|
1332
|
-
const edgeMatch = options?.edgeMatch;
|
|
1333
|
-
const nodesA = [...graphA.nodes].sort((a, b) => {
|
|
1334
|
-
const sigDiff = getDegreeSignature(graphA, b.id).localeCompare(getDegreeSignature(graphA, a.id));
|
|
1335
|
-
if (sigDiff !== 0) return sigDiff;
|
|
1336
|
-
return a.id.localeCompare(b.id);
|
|
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;
|
|
1368
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1369
|
+
const components = [];
|
|
1370
|
+
for (const node of graph.nodes) {
|
|
1371
|
+
if (visited.has(node.id)) continue;
|
|
1372
|
+
const component = [];
|
|
1373
|
+
const queue = [node.id];
|
|
1374
|
+
visited.add(node.id);
|
|
1375
|
+
while (queue.length > 0) {
|
|
1376
|
+
const id = queue.shift();
|
|
1377
|
+
const ni = idx.nodeById.get(id);
|
|
1378
|
+
if (ni !== void 0) component.push(graph.nodes[ni]);
|
|
1379
|
+
for (const eid of idx.outEdges.get(id) ?? []) {
|
|
1380
|
+
const ai = idx.edgeById.get(eid);
|
|
1381
|
+
if (ai === void 0) continue;
|
|
1382
|
+
const neighborId = graph.edges[ai].targetId;
|
|
1383
|
+
if (!visited.has(neighborId)) {
|
|
1384
|
+
visited.add(neighborId);
|
|
1385
|
+
queue.push(neighborId);
|
|
1357
1386
|
}
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1387
|
+
}
|
|
1388
|
+
for (const eid of idx.inEdges.get(id) ?? []) {
|
|
1389
|
+
const ai = idx.edgeById.get(eid);
|
|
1390
|
+
if (ai === void 0) continue;
|
|
1391
|
+
const neighborId = graph.edges[ai].sourceId;
|
|
1392
|
+
if (!visited.has(neighborId)) {
|
|
1393
|
+
visited.add(neighborId);
|
|
1394
|
+
queue.push(neighborId);
|
|
1361
1395
|
}
|
|
1362
1396
|
}
|
|
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
1397
|
}
|
|
1370
|
-
|
|
1371
|
-
}
|
|
1372
|
-
return
|
|
1398
|
+
components.push(component);
|
|
1399
|
+
}
|
|
1400
|
+
return components;
|
|
1373
1401
|
}
|
|
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) {
|
|
1402
|
+
function getTopologicalSort(graph) {
|
|
1397
1403
|
const idx = getIndex(graph);
|
|
1398
|
-
const
|
|
1399
|
-
const
|
|
1400
|
-
|
|
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);
|
|
1407
|
+
const queue = [];
|
|
1408
|
+
for (const [id, degree] of inDegree) if (degree === 0) queue.push(id);
|
|
1409
|
+
const result = [];
|
|
1401
1410
|
while (queue.length > 0) {
|
|
1402
1411
|
const id = queue.shift();
|
|
1403
1412
|
const ni = idx.nodeById.get(id);
|
|
1404
|
-
if (ni
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1413
|
+
if (ni !== void 0) result.push(graph.nodes[ni]);
|
|
1414
|
+
for (const eid of idx.outEdges.get(id) ?? []) {
|
|
1415
|
+
const ai = idx.edgeById.get(eid);
|
|
1416
|
+
if (ai === void 0) continue;
|
|
1417
|
+
const targetId = graph.edges[ai].targetId;
|
|
1418
|
+
const nextDegree = (inDegree.get(targetId) ?? 1) - 1;
|
|
1419
|
+
inDegree.set(targetId, nextDegree);
|
|
1420
|
+
if (nextDegree === 0) queue.push(targetId);
|
|
1409
1421
|
}
|
|
1410
1422
|
}
|
|
1423
|
+
if (result.length !== graph.nodes.length) return null;
|
|
1424
|
+
return result;
|
|
1411
1425
|
}
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
* ```
|
|
1430
|
-
*/
|
|
1431
|
-
function* dfs(graph, startId) {
|
|
1426
|
+
function hasPath(graph, sourceId, targetId) {
|
|
1427
|
+
return getShortestPaths(graph, {
|
|
1428
|
+
from: sourceId,
|
|
1429
|
+
to: targetId
|
|
1430
|
+
}).length > 0;
|
|
1431
|
+
}
|
|
1432
|
+
function isConnected(graph) {
|
|
1433
|
+
if (graph.nodes.length === 0) return true;
|
|
1434
|
+
return getConnectedComponents(graph).length <= 1;
|
|
1435
|
+
}
|
|
1436
|
+
function isTree(graph) {
|
|
1437
|
+
return isConnected(graph) && isAcyclic(graph);
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
//#endregion
|
|
1441
|
+
//#region src/algorithms/ordering.ts
|
|
1442
|
+
function getPreorder(graph, opts) {
|
|
1432
1443
|
const idx = getIndex(graph);
|
|
1433
|
-
const
|
|
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]];
|
|
1434
1449
|
const stack = [startId];
|
|
1435
1450
|
while (stack.length > 0) {
|
|
1436
|
-
const
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
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;
|
|
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]);
|
|
1443
1461
|
}
|
|
1462
|
+
return result;
|
|
1444
1463
|
}
|
|
1445
|
-
function
|
|
1464
|
+
function getPostorder(graph, opts) {
|
|
1446
1465
|
const idx = getIndex(graph);
|
|
1447
|
-
const
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
const
|
|
1454
|
-
|
|
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;
|
|
1479
|
+
}
|
|
1480
|
+
visited.add(next);
|
|
1481
|
+
stack.push(next);
|
|
1455
1482
|
}
|
|
1456
|
-
return
|
|
1483
|
+
return result;
|
|
1457
1484
|
}
|
|
1458
|
-
function
|
|
1459
|
-
|
|
1460
|
-
return (idx.outEdges.get(nodeId) ?? []).map((eid) => graph.edges[idx.edgeById.get(eid)].targetId);
|
|
1485
|
+
function getPreorders(graph, opts) {
|
|
1486
|
+
return [...genPreorders(graph, opts)];
|
|
1461
1487
|
}
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
const
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
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]
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
branched = true;
|
|
1526
|
+
break;
|
|
1491
1527
|
}
|
|
1528
|
+
if (!branched) yield preorder;
|
|
1492
1529
|
}
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
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]
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
branched = true;
|
|
1565
|
+
break;
|
|
1504
1566
|
}
|
|
1567
|
+
if (!branched) yield postorder;
|
|
1505
1568
|
}
|
|
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;
|
|
1540
|
-
return true;
|
|
1541
1569
|
}
|
|
1542
|
-
|
|
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
|
+
});
|
|
1597
|
+
}
|
|
1598
|
+
function primMST(graph, getWeight) {
|
|
1599
|
+
if (graph.nodes.length === 0) return [];
|
|
1543
1600
|
const idx = getIndex(graph);
|
|
1544
|
-
const
|
|
1545
|
-
const
|
|
1546
|
-
|
|
1547
|
-
|
|
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) ?? []) {
|
|
1548
1606
|
const ai = idx.edgeById.get(eid);
|
|
1549
1607
|
if (ai === void 0) continue;
|
|
1550
|
-
const
|
|
1551
|
-
if (!
|
|
1552
|
-
|
|
1553
|
-
|
|
1608
|
+
const edge = graph.edges[ai];
|
|
1609
|
+
if (!inMST.has(edge.targetId)) candidates.push({
|
|
1610
|
+
weight: getWeight(edge),
|
|
1611
|
+
edge
|
|
1612
|
+
});
|
|
1554
1613
|
}
|
|
1555
|
-
for (const eid of idx.inEdges.get(
|
|
1614
|
+
if (graph.type === "undirected") for (const eid of idx.inEdges.get(nodeId) ?? []) {
|
|
1556
1615
|
const ai = idx.edgeById.get(eid);
|
|
1557
1616
|
if (ai === void 0) continue;
|
|
1558
|
-
const
|
|
1559
|
-
if (!
|
|
1560
|
-
|
|
1561
|
-
|
|
1617
|
+
const edge = graph.edges[ai];
|
|
1618
|
+
if (!inMST.has(edge.sourceId)) candidates.push({
|
|
1619
|
+
weight: getWeight(edge),
|
|
1620
|
+
edge
|
|
1621
|
+
});
|
|
1562
1622
|
}
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
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;
|
|
1636
|
+
}
|
|
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;
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
//#endregion
|
|
1667
|
+
//#region src/algorithms/centrality.ts
|
|
1668
|
+
function getNodeIds(graph) {
|
|
1669
|
+
return graph.nodes.map((node) => node.id);
|
|
1670
|
+
}
|
|
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;
|
|
1567
1725
|
}
|
|
1568
1726
|
/**
|
|
1569
|
-
* Returns
|
|
1570
|
-
* Treats all edges as undirected for connectivity.
|
|
1727
|
+
* Returns degree centrality scores for all nodes.
|
|
1571
1728
|
*
|
|
1572
|
-
*
|
|
1729
|
+
* Degree centrality is the node degree normalized by `n - 1`.
|
|
1573
1730
|
*
|
|
1574
1731
|
* @example
|
|
1575
1732
|
* ```ts
|
|
1576
|
-
*
|
|
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]]
|
|
1733
|
+
* const scores = getDegreeCentrality(graph);
|
|
1734
|
+
* console.log(scores.a); // 0.5
|
|
1585
1735
|
* ```
|
|
1586
1736
|
*/
|
|
1587
|
-
function
|
|
1737
|
+
function getDegreeCentrality(graph) {
|
|
1738
|
+
const scale = graph.nodes.length > 1 ? 1 / (graph.nodes.length - 1) : 0;
|
|
1588
1739
|
const idx = getIndex(graph);
|
|
1589
|
-
const
|
|
1590
|
-
const
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
const
|
|
1594
|
-
|
|
1595
|
-
visited.add(n.id);
|
|
1596
|
-
while (queue.length > 0) {
|
|
1597
|
-
const id = queue.shift();
|
|
1598
|
-
const ni = idx.nodeById.get(id);
|
|
1599
|
-
if (ni !== void 0) component.push(graph.nodes[ni]);
|
|
1600
|
-
for (const eid of idx.outEdges.get(id) ?? []) {
|
|
1601
|
-
const ai = idx.edgeById.get(eid);
|
|
1602
|
-
if (ai === void 0) continue;
|
|
1603
|
-
const neighborId = graph.edges[ai].targetId;
|
|
1604
|
-
if (!visited.has(neighborId)) {
|
|
1605
|
-
visited.add(neighborId);
|
|
1606
|
-
queue.push(neighborId);
|
|
1607
|
-
}
|
|
1608
|
-
}
|
|
1609
|
-
for (const eid of idx.inEdges.get(id) ?? []) {
|
|
1610
|
-
const ai = idx.edgeById.get(eid);
|
|
1611
|
-
if (ai === void 0) continue;
|
|
1612
|
-
const neighborId = graph.edges[ai].sourceId;
|
|
1613
|
-
if (!visited.has(neighborId)) {
|
|
1614
|
-
visited.add(neighborId);
|
|
1615
|
-
queue.push(neighborId);
|
|
1616
|
-
}
|
|
1617
|
-
}
|
|
1618
|
-
}
|
|
1619
|
-
components.push(component);
|
|
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;
|
|
1620
1746
|
}
|
|
1621
|
-
return
|
|
1747
|
+
return scores;
|
|
1622
1748
|
}
|
|
1623
1749
|
/**
|
|
1624
|
-
* Returns
|
|
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
|
-
* });
|
|
1750
|
+
* Returns in-degree centrality scores for all nodes.
|
|
1639
1751
|
*
|
|
1640
|
-
*
|
|
1641
|
-
* // [nodeA, nodeB, nodeC]
|
|
1642
|
-
* ```
|
|
1752
|
+
* In-degree centrality is the incoming degree normalized by `n - 1`.
|
|
1643
1753
|
*/
|
|
1644
|
-
function
|
|
1754
|
+
function getInDegreeCentrality(graph) {
|
|
1755
|
+
const scale = graph.nodes.length > 1 ? 1 / (graph.nodes.length - 1) : 0;
|
|
1645
1756
|
const idx = getIndex(graph);
|
|
1646
|
-
const
|
|
1647
|
-
for (const
|
|
1648
|
-
|
|
1649
|
-
const queue = [];
|
|
1650
|
-
for (const [id, deg] of inDeg) if (deg === 0) queue.push(id);
|
|
1651
|
-
const result = [];
|
|
1652
|
-
while (queue.length > 0) {
|
|
1653
|
-
const id = queue.shift();
|
|
1654
|
-
const ni = idx.nodeById.get(id);
|
|
1655
|
-
if (ni !== void 0) result.push(graph.nodes[ni]);
|
|
1656
|
-
for (const eid of idx.outEdges.get(id) ?? []) {
|
|
1657
|
-
const ai = idx.edgeById.get(eid);
|
|
1658
|
-
if (ai === void 0) continue;
|
|
1659
|
-
const targetId = graph.edges[ai].targetId;
|
|
1660
|
-
const newDeg = (inDeg.get(targetId) ?? 1) - 1;
|
|
1661
|
-
inDeg.set(targetId, newDeg);
|
|
1662
|
-
if (newDeg === 0) queue.push(targetId);
|
|
1663
|
-
}
|
|
1664
|
-
}
|
|
1665
|
-
if (result.length !== graph.nodes.length) return null;
|
|
1666
|
-
return result;
|
|
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;
|
|
1667
1760
|
}
|
|
1668
1761
|
/**
|
|
1669
|
-
*
|
|
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
|
-
* });
|
|
1762
|
+
* Returns out-degree centrality scores for all nodes.
|
|
1681
1763
|
*
|
|
1682
|
-
*
|
|
1683
|
-
* hasPath(graph, 'a', 'c'); // false
|
|
1684
|
-
* ```
|
|
1764
|
+
* Out-degree centrality is the outgoing degree normalized by `n - 1`.
|
|
1685
1765
|
*/
|
|
1686
|
-
function
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1766
|
+
function getOutDegreeCentrality(graph) {
|
|
1767
|
+
const scale = graph.nodes.length > 1 ? 1 / (graph.nodes.length - 1) : 0;
|
|
1768
|
+
const idx = getIndex(graph);
|
|
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;
|
|
1691
1772
|
}
|
|
1692
1773
|
/**
|
|
1693
|
-
*
|
|
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
|
-
* });
|
|
1774
|
+
* Returns closeness centrality scores for all nodes.
|
|
1705
1775
|
*
|
|
1706
|
-
*
|
|
1707
|
-
*
|
|
1776
|
+
* Distances are computed over unweighted shortest paths using the graph's
|
|
1777
|
+
* existing directed or undirected edge semantics.
|
|
1708
1778
|
*/
|
|
1709
|
-
function
|
|
1710
|
-
|
|
1711
|
-
|
|
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;
|
|
1712
1793
|
}
|
|
1713
1794
|
/**
|
|
1714
|
-
*
|
|
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
|
-
* });
|
|
1795
|
+
* Returns betweenness centrality scores for all nodes.
|
|
1729
1796
|
*
|
|
1730
|
-
*
|
|
1731
|
-
*
|
|
1797
|
+
* Uses Brandes' algorithm over unweighted shortest paths and returns
|
|
1798
|
+
* normalized scores.
|
|
1732
1799
|
*/
|
|
1733
|
-
function
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
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) {
|
|
1749
|
-
const idx = getIndex(graph);
|
|
1750
|
-
const result = [];
|
|
1751
|
-
for (const eid of idx.outEdges.get(nodeId) ?? []) {
|
|
1752
|
-
const ai = idx.edgeById.get(eid);
|
|
1753
|
-
if (ai !== void 0) {
|
|
1754
|
-
const e = graph.edges[ai];
|
|
1755
|
-
result.push({
|
|
1756
|
-
neighborId: e.targetId,
|
|
1757
|
-
edge: e
|
|
1758
|
-
});
|
|
1759
|
-
}
|
|
1760
|
-
}
|
|
1761
|
-
if (graph.type === "undirected") for (const eid of idx.inEdges.get(nodeId) ?? []) {
|
|
1762
|
-
const ai = idx.edgeById.get(eid);
|
|
1763
|
-
if (ai !== void 0) {
|
|
1764
|
-
const e = graph.edges[ai];
|
|
1765
|
-
result.push({
|
|
1766
|
-
neighborId: e.sourceId,
|
|
1767
|
-
edge: e
|
|
1768
|
-
});
|
|
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);
|
|
1769
1812
|
}
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
}
|
|
1773
|
-
/**
|
|
1774
|
-
* Returns all shortest paths from a source node.
|
|
1775
|
-
* Returns all paths of equal minimum length per target (not just one).
|
|
1776
|
-
* Uses BFS when all edges are unweighted; Dijkstra otherwise.
|
|
1777
|
-
*/
|
|
1778
|
-
/** Compute distance + prev maps via BFS, Dijkstra, or Bellman-Ford. */
|
|
1779
|
-
function computeShortestDistances(graph, sourceId, getWeight, algorithm) {
|
|
1780
|
-
if (algorithm === "bellman-ford") return bellmanFord(graph, sourceId, getWeight);
|
|
1781
|
-
const dist = /* @__PURE__ */ new Map();
|
|
1782
|
-
const prev = /* @__PURE__ */ new Map();
|
|
1783
|
-
dist.set(sourceId, 0);
|
|
1784
|
-
prev.set(sourceId, []);
|
|
1785
|
-
if (!getWeight && !graph.edges.some((e) => e.weight !== void 0)) {
|
|
1786
|
-
const queue = [sourceId];
|
|
1813
|
+
sigma.set(source.id, 1);
|
|
1814
|
+
distance.set(source.id, 0);
|
|
1787
1815
|
while (queue.length > 0) {
|
|
1788
|
-
const
|
|
1789
|
-
|
|
1790
|
-
for (const
|
|
1791
|
-
|
|
1792
|
-
const existing = dist.get(neighborId);
|
|
1793
|
-
if (existing === void 0) {
|
|
1794
|
-
dist.set(neighborId, newDist);
|
|
1795
|
-
prev.set(neighborId, [{
|
|
1796
|
-
from: id,
|
|
1797
|
-
edge
|
|
1798
|
-
}]);
|
|
1816
|
+
const currentId = queue.shift();
|
|
1817
|
+
stack.push(currentId);
|
|
1818
|
+
for (const neighborId of getNeighborIds(graph, currentId)) {
|
|
1819
|
+
if (distance.get(neighborId) === -1) {
|
|
1799
1820
|
queue.push(neighborId);
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1821
|
+
distance.set(neighborId, distance.get(currentId) + 1);
|
|
1822
|
+
}
|
|
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);
|
|
1826
|
+
}
|
|
1804
1827
|
}
|
|
1805
1828
|
}
|
|
1806
|
-
|
|
1807
|
-
const
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
const { id, dist: d } = pq.pop();
|
|
1816
|
-
if (visited.has(id) || d !== dist.get(id)) continue;
|
|
1817
|
-
visited.add(id);
|
|
1818
|
-
for (const { neighborId, edge } of getNeighborEdges(graph, id)) {
|
|
1819
|
-
const newDist = d + effectiveWeight(edge);
|
|
1820
|
-
const existing = dist.get(neighborId);
|
|
1821
|
-
if (existing === void 0 || newDist < existing) {
|
|
1822
|
-
dist.set(neighborId, newDist);
|
|
1823
|
-
prev.set(neighborId, [{
|
|
1824
|
-
from: id,
|
|
1825
|
-
edge
|
|
1826
|
-
}]);
|
|
1827
|
-
pq.push({
|
|
1828
|
-
id: neighborId,
|
|
1829
|
-
dist: newDist
|
|
1830
|
-
});
|
|
1831
|
-
} else if (existing === newDist) prev.get(neighborId).push({
|
|
1832
|
-
from: id,
|
|
1833
|
-
edge
|
|
1834
|
-
});
|
|
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);
|
|
1835
1838
|
}
|
|
1839
|
+
if (nodeId !== source.id) scores[nodeId] += delta.get(nodeId);
|
|
1836
1840
|
}
|
|
1837
1841
|
}
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
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;
|
|
1848
|
+
}
|
|
1849
|
+
return scores;
|
|
1842
1850
|
}
|
|
1843
1851
|
/**
|
|
1844
|
-
*
|
|
1845
|
-
*
|
|
1846
|
-
*
|
|
1852
|
+
* Returns PageRank scores for all nodes.
|
|
1853
|
+
*
|
|
1854
|
+
* Uses power iteration with damping factor `alpha`.
|
|
1847
1855
|
*/
|
|
1848
|
-
function
|
|
1849
|
-
const
|
|
1850
|
-
|
|
1851
|
-
const
|
|
1852
|
-
const
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
toId: edge.targetId,
|
|
1864
|
-
edge
|
|
1865
|
-
});
|
|
1866
|
-
if (isUndirected) directedEdges.push({
|
|
1867
|
-
fromId: edge.targetId,
|
|
1868
|
-
toId: edge.sourceId,
|
|
1869
|
-
edge
|
|
1870
|
-
});
|
|
1871
|
-
}
|
|
1872
|
-
for (let i = 0; i < V - 1; i++) {
|
|
1873
|
-
let changed = false;
|
|
1874
|
-
for (const { fromId, toId, edge } of directedEdges) {
|
|
1875
|
-
const d = dist.get(fromId);
|
|
1876
|
-
if (d === Infinity) continue;
|
|
1877
|
-
const newDist = d + effectiveWeight(edge);
|
|
1878
|
-
const existing = dist.get(toId);
|
|
1879
|
-
if (newDist < existing) {
|
|
1880
|
-
dist.set(toId, newDist);
|
|
1881
|
-
prev.set(toId, [{
|
|
1882
|
-
from: fromId,
|
|
1883
|
-
edge
|
|
1884
|
-
}]);
|
|
1885
|
-
changed = true;
|
|
1886
|
-
} else if (newDist === existing && existing !== Infinity) {
|
|
1887
|
-
const preds = prev.get(toId);
|
|
1888
|
-
if (!preds.some((p) => p.from === fromId && p.edge === edge)) preds.push({
|
|
1889
|
-
from: fromId,
|
|
1890
|
-
edge
|
|
1891
|
-
});
|
|
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;
|
|
1892
1871
|
}
|
|
1872
|
+
const share = scores[nodeId] / neighbors.length;
|
|
1873
|
+
for (const neighborId of neighbors) nextScores[neighborId] += alpha * share;
|
|
1893
1874
|
}
|
|
1894
|
-
if (
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
if (
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
prev.delete(id);
|
|
1904
|
-
}
|
|
1905
|
-
return {
|
|
1906
|
-
dist,
|
|
1907
|
-
prev
|
|
1908
|
-
};
|
|
1909
|
-
}
|
|
1910
|
-
/** Reconstruct all shortest paths to a target by backtracking through prev map. */
|
|
1911
|
-
function* reconstructPaths(graph, prev, sourceNode, targetId) {
|
|
1912
|
-
if (targetId === sourceNode.id) {
|
|
1913
|
-
yield {
|
|
1914
|
-
source: sourceNode,
|
|
1915
|
-
steps: []
|
|
1916
|
-
};
|
|
1917
|
-
return;
|
|
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;
|
|
1918
1884
|
}
|
|
1919
|
-
const
|
|
1920
|
-
if (
|
|
1921
|
-
|
|
1922
|
-
const targetNode = targetNi !== void 0 ? graph.nodes[targetNi] : graph.nodes.find((n) => n.id === targetId);
|
|
1923
|
-
for (const { from, edge } of preds) for (const prefix of reconstructPaths(graph, prev, sourceNode, from)) yield {
|
|
1924
|
-
source: sourceNode,
|
|
1925
|
-
steps: [...prefix.steps, {
|
|
1926
|
-
edge,
|
|
1927
|
-
node: targetNode
|
|
1928
|
-
}]
|
|
1929
|
-
};
|
|
1930
|
-
}
|
|
1931
|
-
/**
|
|
1932
|
-
* Lazily yields all shortest paths from a source node.
|
|
1933
|
-
* Use `getShortestPaths` for the full array.
|
|
1934
|
-
*
|
|
1935
|
-
* **O(V + E)** time (BFS) or **O((V + E) log V)** (Dijkstra when weighted),
|
|
1936
|
-
* plus **O(P)** per path yielded where P is the path length.
|
|
1937
|
-
*
|
|
1938
|
-
* @example
|
|
1939
|
-
* ```ts
|
|
1940
|
-
* import { createGraph, genShortestPaths } from '@statelyai/graph';
|
|
1941
|
-
*
|
|
1942
|
-
* const graph = createGraph({
|
|
1943
|
-
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1944
|
-
* edges: [
|
|
1945
|
-
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1946
|
-
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
1947
|
-
* ],
|
|
1948
|
-
* initialNodeId: 'a',
|
|
1949
|
-
* });
|
|
1950
|
-
*
|
|
1951
|
-
* for (const path of genShortestPaths(graph)) {
|
|
1952
|
-
* console.log(path.steps.map(s => s.node.id));
|
|
1953
|
-
* }
|
|
1954
|
-
* ```
|
|
1955
|
-
*/
|
|
1956
|
-
function* genShortestPaths(graph, opts) {
|
|
1957
|
-
const idx = getIndex(graph);
|
|
1958
|
-
const sourceId = resolveFrom(graph, opts);
|
|
1959
|
-
const { dist, prev } = computeShortestDistances(graph, sourceId, opts?.getWeight, opts?.algorithm);
|
|
1960
|
-
const targets = opts?.to ? [opts.to].filter((id) => dist.has(id)) : [...dist.keys()].filter((id) => id !== sourceId);
|
|
1961
|
-
const sourceNi = idx.nodeById.get(sourceId);
|
|
1962
|
-
const sourceNode = sourceNi !== void 0 ? graph.nodes[sourceNi] : graph.nodes.find((n) => n.id === sourceId);
|
|
1963
|
-
for (const targetId of targets) yield* reconstructPaths(graph, prev, sourceNode, targetId);
|
|
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;
|
|
1964
1888
|
}
|
|
1965
1889
|
/**
|
|
1966
|
-
* Returns
|
|
1967
|
-
* Delegates to `genShortestPaths` internally.
|
|
1968
|
-
*
|
|
1969
|
-
* **O(V + E)** time (BFS) or **O((V + E) log V)** (Dijkstra when weighted).
|
|
1970
|
-
*
|
|
1971
|
-
* @example
|
|
1972
|
-
* ```ts
|
|
1973
|
-
* import { createGraph, getShortestPaths } from '@statelyai/graph';
|
|
1974
|
-
*
|
|
1975
|
-
* const graph = createGraph({
|
|
1976
|
-
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1977
|
-
* edges: [
|
|
1978
|
-
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1979
|
-
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
1980
|
-
* ],
|
|
1981
|
-
* initialNodeId: 'a',
|
|
1982
|
-
* });
|
|
1890
|
+
* Returns HITS hub and authority scores for all nodes.
|
|
1983
1891
|
*
|
|
1984
|
-
*
|
|
1985
|
-
* // paths to 'b' and 'c' from 'a'
|
|
1986
|
-
* ```
|
|
1892
|
+
* Uses power iteration and L2 normalization per iteration.
|
|
1987
1893
|
*/
|
|
1988
|
-
function
|
|
1989
|
-
|
|
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;
|
|
1916
|
+
}
|
|
1917
|
+
return {
|
|
1918
|
+
hubs,
|
|
1919
|
+
authorities
|
|
1920
|
+
};
|
|
1990
1921
|
}
|
|
1991
1922
|
/**
|
|
1992
|
-
* Returns
|
|
1993
|
-
*
|
|
1994
|
-
* **O(V + E)** time (BFS) or **O((V + E) log V)** (Dijkstra when weighted).
|
|
1995
|
-
*
|
|
1996
|
-
* @example
|
|
1997
|
-
* ```ts
|
|
1998
|
-
* import { createGraph, getShortestPath } from '@statelyai/graph';
|
|
1999
|
-
*
|
|
2000
|
-
* const graph = createGraph({
|
|
2001
|
-
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
2002
|
-
* edges: [
|
|
2003
|
-
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
2004
|
-
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
2005
|
-
* ],
|
|
2006
|
-
* initialNodeId: 'a',
|
|
2007
|
-
* });
|
|
1923
|
+
* Returns eigenvector centrality scores for all nodes.
|
|
2008
1924
|
*
|
|
2009
|
-
*
|
|
2010
|
-
*
|
|
2011
|
-
* ```
|
|
1925
|
+
* Uses power iteration over incoming neighbors for directed graphs and
|
|
1926
|
+
* undirected adjacency for undirected graphs.
|
|
2012
1927
|
*/
|
|
2013
|
-
function
|
|
2014
|
-
|
|
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;
|
|
2015
1944
|
}
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
* // two paths: a->b->c and a->c
|
|
2038
|
-
* ```
|
|
2039
|
-
*/
|
|
2040
|
-
function getSimplePaths(graph, opts) {
|
|
2041
|
-
return [...genSimplePaths(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;
|
|
2042
1966
|
}
|
|
2043
|
-
|
|
2044
|
-
* Lazily yields all simple (acyclic) paths from a source node via DFS backtracking.
|
|
2045
|
-
* Use `getSimplePaths` for the full array.
|
|
2046
|
-
*
|
|
2047
|
-
* **O(V!)** worst-case (exponential in dense graphs).
|
|
2048
|
-
*
|
|
2049
|
-
* @example
|
|
2050
|
-
* ```ts
|
|
2051
|
-
* import { createGraph, genSimplePaths } from '@statelyai/graph';
|
|
2052
|
-
*
|
|
2053
|
-
* const graph = createGraph({
|
|
2054
|
-
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
2055
|
-
* edges: [
|
|
2056
|
-
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
2057
|
-
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
2058
|
-
* { id: 'ac', sourceId: 'a', targetId: 'c' },
|
|
2059
|
-
* ],
|
|
2060
|
-
* initialNodeId: 'a',
|
|
2061
|
-
* });
|
|
2062
|
-
*
|
|
2063
|
-
* for (const path of genSimplePaths(graph, { to: 'c' })) {
|
|
2064
|
-
* console.log(path.steps.map(s => s.node.id));
|
|
2065
|
-
* // ['b', 'c'] or ['c']
|
|
2066
|
-
* }
|
|
2067
|
-
* ```
|
|
2068
|
-
*/
|
|
2069
|
-
function* genSimplePaths(graph, opts) {
|
|
1967
|
+
function getUndirectedConnectedComponents(graph) {
|
|
2070
1968
|
const idx = getIndex(graph);
|
|
2071
|
-
const sourceId = resolveFrom(graph, opts);
|
|
2072
|
-
const sourceNi = idx.nodeById.get(sourceId);
|
|
2073
|
-
const sourceNode = sourceNi !== void 0 ? graph.nodes[sourceNi] : graph.nodes.find((n) => n.id === sourceId);
|
|
2074
|
-
const targetId = opts?.to;
|
|
2075
1969
|
const visited = /* @__PURE__ */ new Set();
|
|
2076
|
-
const
|
|
2077
|
-
|
|
2078
|
-
visited.
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
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);
|
|
2087
1984
|
}
|
|
2088
|
-
} else if (currentSteps.length > 0) yield {
|
|
2089
|
-
source: sourceNode,
|
|
2090
|
-
steps: [...currentSteps]
|
|
2091
|
-
};
|
|
2092
|
-
for (const { neighborId, edge } of getNeighborEdges(graph, nodeId)) if (!visited.has(neighborId)) {
|
|
2093
|
-
const neighborNi = idx.nodeById.get(neighborId);
|
|
2094
|
-
const neighborNode = neighborNi !== void 0 ? graph.nodes[neighborNi] : graph.nodes.find((n) => n.id === neighborId);
|
|
2095
|
-
currentSteps.push({
|
|
2096
|
-
edge,
|
|
2097
|
-
node: neighborNode
|
|
2098
|
-
});
|
|
2099
|
-
yield* dfsCollect(neighborId);
|
|
2100
|
-
currentSteps.pop();
|
|
2101
1985
|
}
|
|
2102
|
-
|
|
1986
|
+
communities.push(community.sort((a, b) => a.id.localeCompare(b.id)));
|
|
2103
1987
|
}
|
|
2104
|
-
|
|
1988
|
+
return communities.sort((a, b) => a[0].id.localeCompare(b[0].id));
|
|
2105
1989
|
}
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
*
|
|
2109
|
-
* **O(V + E)** typical, **O(V!)** worst-case.
|
|
2110
|
-
*
|
|
2111
|
-
* @example
|
|
2112
|
-
* ```ts
|
|
2113
|
-
* import { createGraph, getSimplePath } from '@statelyai/graph';
|
|
2114
|
-
*
|
|
2115
|
-
* const graph = createGraph({
|
|
2116
|
-
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
2117
|
-
* edges: [
|
|
2118
|
-
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
2119
|
-
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
2120
|
-
* ],
|
|
2121
|
-
* initialNodeId: 'a',
|
|
2122
|
-
* });
|
|
2123
|
-
*
|
|
2124
|
-
* const path = getSimplePath(graph, { to: 'c' });
|
|
2125
|
-
* // path.steps -> [{node: nodeB, edge: ...}, {node: nodeC, edge: ...}]
|
|
2126
|
-
* ```
|
|
2127
|
-
*/
|
|
2128
|
-
function getSimplePath(graph, opts) {
|
|
2129
|
-
for (const path of genSimplePaths(graph, opts)) return path;
|
|
1990
|
+
function getNodeMap(graph) {
|
|
1991
|
+
return new Map(graph.nodes.map((node) => [node.id, node]));
|
|
2130
1992
|
}
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
* ```ts
|
|
2139
|
-
* import { createGraph, getStronglyConnectedComponents } from '@statelyai/graph';
|
|
2140
|
-
*
|
|
2141
|
-
* const graph = createGraph({
|
|
2142
|
-
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
2143
|
-
* edges: [
|
|
2144
|
-
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
2145
|
-
* { id: 'ba', sourceId: 'b', targetId: 'a' },
|
|
2146
|
-
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
2147
|
-
* ],
|
|
2148
|
-
* });
|
|
2149
|
-
*
|
|
2150
|
-
* const sccs = getStronglyConnectedComponents(graph);
|
|
2151
|
-
* // [[nodeA, nodeB], [nodeC]]
|
|
2152
|
-
* ```
|
|
2153
|
-
*/
|
|
2154
|
-
function getStronglyConnectedComponents(graph) {
|
|
2155
|
-
const idx = getIndex(graph);
|
|
2156
|
-
let indexCounter = 0;
|
|
2157
|
-
const nodeIndex = /* @__PURE__ */ new Map();
|
|
2158
|
-
const lowlink = /* @__PURE__ */ new Map();
|
|
2159
|
-
const onStack = /* @__PURE__ */ new Set();
|
|
2160
|
-
const stack = [];
|
|
2161
|
-
const result = [];
|
|
2162
|
-
function strongconnect(id) {
|
|
2163
|
-
nodeIndex.set(id, indexCounter);
|
|
2164
|
-
lowlink.set(id, indexCounter);
|
|
2165
|
-
indexCounter++;
|
|
2166
|
-
stack.push(id);
|
|
2167
|
-
onStack.add(id);
|
|
2168
|
-
for (const eid of idx.outEdges.get(id) ?? []) {
|
|
2169
|
-
const ai = idx.edgeById.get(eid);
|
|
2170
|
-
if (ai === void 0) continue;
|
|
2171
|
-
const wId = graph.edges[ai].targetId;
|
|
2172
|
-
if (!nodeIndex.has(wId)) {
|
|
2173
|
-
strongconnect(wId);
|
|
2174
|
-
lowlink.set(id, Math.min(lowlink.get(id), lowlink.get(wId)));
|
|
2175
|
-
} else if (onStack.has(wId)) lowlink.set(id, Math.min(lowlink.get(id), nodeIndex.get(wId)));
|
|
2176
|
-
}
|
|
2177
|
-
if (lowlink.get(id) === nodeIndex.get(id)) {
|
|
2178
|
-
const component = [];
|
|
2179
|
-
let wId;
|
|
2180
|
-
do {
|
|
2181
|
-
wId = stack.pop();
|
|
2182
|
-
onStack.delete(wId);
|
|
2183
|
-
const ni = idx.nodeById.get(wId);
|
|
2184
|
-
if (ni !== void 0) component.push(graph.nodes[ni]);
|
|
2185
|
-
} while (wId !== id);
|
|
2186
|
-
result.push(component);
|
|
2187
|
-
}
|
|
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);
|
|
2188
2000
|
}
|
|
2189
|
-
|
|
2190
|
-
return result;
|
|
2191
|
-
}
|
|
2192
|
-
/**
|
|
2193
|
-
* Returns all elementary cycles as an array of paths.
|
|
2194
|
-
* Delegates to `genCycles` internally.
|
|
2195
|
-
*
|
|
2196
|
-
* **O((V + E) · C)** where C is the number of elementary cycles (can be exponential).
|
|
2197
|
-
*
|
|
2198
|
-
* @example
|
|
2199
|
-
* ```ts
|
|
2200
|
-
* import { createGraph, getCycles } from '@statelyai/graph';
|
|
2201
|
-
*
|
|
2202
|
-
* const graph = createGraph({
|
|
2203
|
-
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
2204
|
-
* edges: [
|
|
2205
|
-
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
2206
|
-
* { id: 'ba', sourceId: 'b', targetId: 'a' },
|
|
2207
|
-
* ],
|
|
2208
|
-
* });
|
|
2209
|
-
*
|
|
2210
|
-
* const cycles = getCycles(graph);
|
|
2211
|
-
* // one cycle: a -> b -> a
|
|
2212
|
-
* ```
|
|
2213
|
-
*/
|
|
2214
|
-
function getCycles(graph) {
|
|
2215
|
-
return [...genCycles(graph)];
|
|
2216
|
-
}
|
|
2217
|
-
/**
|
|
2218
|
-
* Lazily yields elementary cycles one at a time.
|
|
2219
|
-
* Use `getCycles` for the full array.
|
|
2220
|
-
*
|
|
2221
|
-
* **O((V + E) · C)** where C is the number of elementary cycles (can be exponential).
|
|
2222
|
-
*
|
|
2223
|
-
* @example
|
|
2224
|
-
* ```ts
|
|
2225
|
-
* import { createGraph, genCycles } from '@statelyai/graph';
|
|
2226
|
-
*
|
|
2227
|
-
* const graph = createGraph({
|
|
2228
|
-
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
2229
|
-
* edges: [
|
|
2230
|
-
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
2231
|
-
* { id: 'ba', sourceId: 'b', targetId: 'a' },
|
|
2232
|
-
* ],
|
|
2233
|
-
* });
|
|
2234
|
-
*
|
|
2235
|
-
* for (const cycle of genCycles(graph)) {
|
|
2236
|
-
* console.log(cycle.steps.map(s => s.node.id)); // ['b', 'a']
|
|
2237
|
-
* }
|
|
2238
|
-
* ```
|
|
2239
|
-
*/
|
|
2240
|
-
function* genCycles(graph) {
|
|
2241
|
-
if (graph.type === "undirected") yield* genCyclesUndirected(graph);
|
|
2242
|
-
else yield* genCyclesDirected(graph);
|
|
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));
|
|
2243
2002
|
}
|
|
2244
|
-
function
|
|
2245
|
-
const
|
|
2246
|
-
const
|
|
2247
|
-
|
|
2248
|
-
const
|
|
2249
|
-
const
|
|
2250
|
-
const
|
|
2251
|
-
const
|
|
2252
|
-
const
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
visited.add(currentId);
|
|
2257
|
-
for (const eid of idx.outEdges.get(currentId) ?? []) {
|
|
2258
|
-
const ai = idx.edgeById.get(eid);
|
|
2259
|
-
if (ai === void 0) continue;
|
|
2260
|
-
const e = graph.edges[ai];
|
|
2261
|
-
const neighborId = e.targetId;
|
|
2262
|
-
if (neighborId === startId && (steps.length > 0 || currentId === startId)) found.push({
|
|
2263
|
-
source: startNode,
|
|
2264
|
-
steps: [...steps, {
|
|
2265
|
-
edge: e,
|
|
2266
|
-
node: startNode
|
|
2267
|
-
}]
|
|
2268
|
-
});
|
|
2269
|
-
else if (allowed.has(neighborId) && !visited.has(neighborId)) {
|
|
2270
|
-
const ni = idx.nodeById.get(neighborId);
|
|
2271
|
-
steps.push({
|
|
2272
|
-
edge: e,
|
|
2273
|
-
node: graph.nodes[ni]
|
|
2274
|
-
});
|
|
2275
|
-
dfsFind(neighborId);
|
|
2276
|
-
steps.pop();
|
|
2277
|
-
}
|
|
2278
|
-
}
|
|
2279
|
-
visited.delete(currentId);
|
|
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);
|
|
2280
2015
|
}
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
const startId = sortedIds[si];
|
|
2291
|
-
const allowed = new Set(sortedIds.slice(si));
|
|
2292
|
-
const visited = /* @__PURE__ */ new Set();
|
|
2293
|
-
const steps = [];
|
|
2294
|
-
const startNi = idx.nodeById.get(startId);
|
|
2295
|
-
const startNode = graph.nodes[startNi];
|
|
2296
|
-
const found = [];
|
|
2297
|
-
function dfsFind(currentId, parentId) {
|
|
2298
|
-
visited.add(currentId);
|
|
2299
|
-
for (const { neighborId, edge } of getNeighborEdgesAll(graph, currentId)) {
|
|
2300
|
-
if (neighborId === parentId) {
|
|
2301
|
-
parentId = null;
|
|
2302
|
-
continue;
|
|
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);
|
|
2303
2025
|
}
|
|
2304
|
-
if (
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
source: startNode,
|
|
2310
|
-
steps: [...steps, {
|
|
2311
|
-
edge,
|
|
2312
|
-
node: startNode
|
|
2313
|
-
}]
|
|
2314
|
-
});
|
|
2315
|
-
}
|
|
2316
|
-
} else if (allowed.has(neighborId) && !visited.has(neighborId)) {
|
|
2317
|
-
const ni = idx.nodeById.get(neighborId);
|
|
2318
|
-
steps.push({
|
|
2319
|
-
edge,
|
|
2320
|
-
node: graph.nodes[ni]
|
|
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
|
|
2321
2031
|
});
|
|
2322
|
-
dfsFind(neighborId, currentId);
|
|
2323
|
-
steps.pop();
|
|
2324
2032
|
}
|
|
2325
2033
|
}
|
|
2326
|
-
visited.delete(currentId);
|
|
2327
|
-
}
|
|
2328
|
-
dfsFind(startId, null);
|
|
2329
|
-
yield* found;
|
|
2330
|
-
}
|
|
2331
|
-
}
|
|
2332
|
-
/** Like getNeighborEdges but always includes both directions (for undirected cycle finding). */
|
|
2333
|
-
function getNeighborEdgesAll(graph, nodeId) {
|
|
2334
|
-
const idx = getIndex(graph);
|
|
2335
|
-
const result = [];
|
|
2336
|
-
for (const eid of idx.outEdges.get(nodeId) ?? []) {
|
|
2337
|
-
const ai = idx.edgeById.get(eid);
|
|
2338
|
-
if (ai !== void 0) {
|
|
2339
|
-
const e = graph.edges[ai];
|
|
2340
|
-
result.push({
|
|
2341
|
-
neighborId: e.targetId,
|
|
2342
|
-
edge: e
|
|
2343
|
-
});
|
|
2344
|
-
}
|
|
2345
|
-
}
|
|
2346
|
-
for (const eid of idx.inEdges.get(nodeId) ?? []) {
|
|
2347
|
-
const ai = idx.edgeById.get(eid);
|
|
2348
|
-
if (ai !== void 0) {
|
|
2349
|
-
const e = graph.edges[ai];
|
|
2350
|
-
result.push({
|
|
2351
|
-
neighborId: e.sourceId,
|
|
2352
|
-
edge: e
|
|
2353
|
-
});
|
|
2354
|
-
}
|
|
2355
|
-
}
|
|
2356
|
-
return result;
|
|
2357
|
-
}
|
|
2358
|
-
/**
|
|
2359
|
-
* Returns a single canonical preorder (DFS visit-order) sequence.
|
|
2360
|
-
* Visits neighbors in the order they appear in the adjacency list.
|
|
2361
|
-
*
|
|
2362
|
-
* **O(V + E)** time.
|
|
2363
|
-
*
|
|
2364
|
-
* @example
|
|
2365
|
-
* ```ts
|
|
2366
|
-
* import { createGraph, getPreorder } from '@statelyai/graph';
|
|
2367
|
-
*
|
|
2368
|
-
* const graph = createGraph({
|
|
2369
|
-
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
2370
|
-
* edges: [
|
|
2371
|
-
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
2372
|
-
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
2373
|
-
* ],
|
|
2374
|
-
* initialNodeId: 'a',
|
|
2375
|
-
* });
|
|
2376
|
-
*
|
|
2377
|
-
* const order = getPreorder(graph);
|
|
2378
|
-
* // [nodeA, nodeB, nodeC]
|
|
2379
|
-
* ```
|
|
2380
|
-
*/
|
|
2381
|
-
function getPreorder(graph, opts) {
|
|
2382
|
-
const idx = getIndex(graph);
|
|
2383
|
-
const startId = resolveFrom(graph, opts);
|
|
2384
|
-
const startNi = idx.nodeById.get(startId);
|
|
2385
|
-
if (startNi === void 0) return [];
|
|
2386
|
-
const visited = new Set([startId]);
|
|
2387
|
-
const result = [graph.nodes[startNi]];
|
|
2388
|
-
const stack = [startId];
|
|
2389
|
-
while (stack.length > 0) {
|
|
2390
|
-
const top = stack[stack.length - 1];
|
|
2391
|
-
const next = getNeighborIds(graph, top).find((id) => !visited.has(id));
|
|
2392
|
-
if (next === void 0) {
|
|
2393
|
-
stack.pop();
|
|
2394
|
-
continue;
|
|
2395
|
-
}
|
|
2396
|
-
visited.add(next);
|
|
2397
|
-
stack.push(next);
|
|
2398
|
-
const ni = idx.nodeById.get(next);
|
|
2399
|
-
if (ni !== void 0) result.push(graph.nodes[ni]);
|
|
2400
|
-
}
|
|
2401
|
-
return result;
|
|
2402
|
-
}
|
|
2403
|
-
/**
|
|
2404
|
-
* Returns a single canonical postorder (DFS finish-order) sequence.
|
|
2405
|
-
* Visits neighbors in the order they appear in the adjacency list.
|
|
2406
|
-
*
|
|
2407
|
-
* **O(V + E)** time.
|
|
2408
|
-
*
|
|
2409
|
-
* @example
|
|
2410
|
-
* ```ts
|
|
2411
|
-
* import { createGraph, getPostorder } from '@statelyai/graph';
|
|
2412
|
-
*
|
|
2413
|
-
* const graph = createGraph({
|
|
2414
|
-
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
2415
|
-
* edges: [
|
|
2416
|
-
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
2417
|
-
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
2418
|
-
* ],
|
|
2419
|
-
* initialNodeId: 'a',
|
|
2420
|
-
* });
|
|
2421
|
-
*
|
|
2422
|
-
* const order = getPostorder(graph);
|
|
2423
|
-
* // [nodeC, nodeB, nodeA]
|
|
2424
|
-
* ```
|
|
2425
|
-
*/
|
|
2426
|
-
function getPostorder(graph, opts) {
|
|
2427
|
-
const idx = getIndex(graph);
|
|
2428
|
-
const startId = resolveFrom(graph, opts);
|
|
2429
|
-
if (idx.nodeById.get(startId) === void 0) return [];
|
|
2430
|
-
const visited = new Set([startId]);
|
|
2431
|
-
const result = [];
|
|
2432
|
-
const stack = [startId];
|
|
2433
|
-
while (stack.length > 0) {
|
|
2434
|
-
const top = stack[stack.length - 1];
|
|
2435
|
-
const next = getNeighborIds(graph, top).find((id) => !visited.has(id));
|
|
2436
|
-
if (next === void 0) {
|
|
2437
|
-
stack.pop();
|
|
2438
|
-
const ni = idx.nodeById.get(top);
|
|
2439
|
-
if (ni !== void 0) result.push(graph.nodes[ni]);
|
|
2440
|
-
continue;
|
|
2441
2034
|
}
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
*
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
* @example
|
|
2453
|
-
* ```ts
|
|
2454
|
-
* import { createGraph, getPreorders } from '@statelyai/graph';
|
|
2455
|
-
*
|
|
2456
|
-
* const graph = createGraph({
|
|
2457
|
-
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
2458
|
-
* edges: [
|
|
2459
|
-
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
2460
|
-
* { id: 'ac', sourceId: 'a', targetId: 'c' },
|
|
2461
|
-
* ],
|
|
2462
|
-
* initialNodeId: 'a',
|
|
2463
|
-
* });
|
|
2464
|
-
*
|
|
2465
|
-
* const allOrders = getPreorders(graph);
|
|
2466
|
-
* // [[nodeA, nodeB, nodeC], [nodeA, nodeC, nodeB]]
|
|
2467
|
-
* ```
|
|
2468
|
-
*/
|
|
2469
|
-
function getPreorders(graph, opts) {
|
|
2470
|
-
return [...genPreorders(graph, opts)];
|
|
2471
|
-
}
|
|
2472
|
-
/**
|
|
2473
|
-
* Returns all possible postorder sequences as an array. Can be exponential -- prefer `genPostorders`.
|
|
2474
|
-
*
|
|
2475
|
-
* **O(V! · V)** worst-case (exponential).
|
|
2476
|
-
*
|
|
2477
|
-
* @example
|
|
2478
|
-
* ```ts
|
|
2479
|
-
* import { createGraph, getPostorders } from '@statelyai/graph';
|
|
2480
|
-
*
|
|
2481
|
-
* const graph = createGraph({
|
|
2482
|
-
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
2483
|
-
* edges: [
|
|
2484
|
-
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
2485
|
-
* { id: 'ac', sourceId: 'a', targetId: 'c' },
|
|
2486
|
-
* ],
|
|
2487
|
-
* initialNodeId: 'a',
|
|
2488
|
-
* });
|
|
2489
|
-
*
|
|
2490
|
-
* const allOrders = getPostorders(graph);
|
|
2491
|
-
* // [[nodeB, nodeC, nodeA], [nodeC, nodeB, nodeA]]
|
|
2492
|
-
* ```
|
|
2493
|
-
*/
|
|
2494
|
-
function getPostorders(graph, opts) {
|
|
2495
|
-
return [...genPostorders(graph, opts)];
|
|
2496
|
-
}
|
|
2497
|
-
/**
|
|
2498
|
-
* Lazily yields all possible preorder (DFS visit-order) sequences.
|
|
2499
|
-
* Different neighbor exploration orders yield different sequences.
|
|
2500
|
-
* Use `getPreorder()` for a single canonical ordering.
|
|
2501
|
-
*
|
|
2502
|
-
* **O(V! · V)** worst-case (exponential).
|
|
2503
|
-
*
|
|
2504
|
-
* @example
|
|
2505
|
-
* ```ts
|
|
2506
|
-
* import { createGraph, genPreorders } from '@statelyai/graph';
|
|
2507
|
-
*
|
|
2508
|
-
* const graph = createGraph({
|
|
2509
|
-
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
2510
|
-
* edges: [
|
|
2511
|
-
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
2512
|
-
* { id: 'ac', sourceId: 'a', targetId: 'c' },
|
|
2513
|
-
* ],
|
|
2514
|
-
* initialNodeId: 'a',
|
|
2515
|
-
* });
|
|
2516
|
-
*
|
|
2517
|
-
* for (const order of genPreorders(graph)) {
|
|
2518
|
-
* console.log(order.map(n => n.id));
|
|
2519
|
-
* // ['a', 'b', 'c'] or ['a', 'c', 'b']
|
|
2520
|
-
* }
|
|
2521
|
-
* ```
|
|
2522
|
-
*/
|
|
2523
|
-
function* genPreorders(graph, opts) {
|
|
2524
|
-
const idx = getIndex(graph);
|
|
2525
|
-
const startId = resolveFrom(graph, opts);
|
|
2526
|
-
const startNi = idx.nodeById.get(startId);
|
|
2527
|
-
const startNode = startNi !== void 0 ? graph.nodes[startNi] : void 0;
|
|
2528
|
-
if (!startNode) return;
|
|
2529
|
-
const queue = [{
|
|
2530
|
-
visited: new Set([startId]),
|
|
2531
|
-
preorder: [startNode],
|
|
2532
|
-
dfsStack: [startId]
|
|
2533
|
-
}];
|
|
2534
|
-
while (queue.length > 0) {
|
|
2535
|
-
const frame = queue.pop();
|
|
2536
|
-
const { visited, dfsStack } = frame;
|
|
2537
|
-
let { preorder } = frame;
|
|
2538
|
-
let branched = false;
|
|
2539
|
-
while (dfsStack.length > 0) {
|
|
2540
|
-
const top = dfsStack[dfsStack.length - 1];
|
|
2541
|
-
const unvisited = getNeighborIds(graph, top).filter((id) => !visited.has(id));
|
|
2542
|
-
if (unvisited.length === 0) {
|
|
2543
|
-
dfsStack.pop();
|
|
2544
|
-
continue;
|
|
2545
|
-
}
|
|
2546
|
-
for (const nextId of unvisited) {
|
|
2547
|
-
const ni = idx.nodeById.get(nextId);
|
|
2548
|
-
if (ni === void 0) continue;
|
|
2549
|
-
const newVisited = new Set(visited);
|
|
2550
|
-
newVisited.add(nextId);
|
|
2551
|
-
queue.push({
|
|
2552
|
-
visited: newVisited,
|
|
2553
|
-
preorder: [...preorder, graph.nodes[ni]],
|
|
2554
|
-
dfsStack: [...dfsStack, nextId]
|
|
2555
|
-
});
|
|
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);
|
|
2556
2045
|
}
|
|
2557
|
-
branched = true;
|
|
2558
|
-
break;
|
|
2559
2046
|
}
|
|
2560
|
-
if (!branched) yield preorder;
|
|
2561
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)));
|
|
2562
2060
|
}
|
|
2563
2061
|
/**
|
|
2564
|
-
*
|
|
2565
|
-
* Different neighbor exploration orders yield different sequences.
|
|
2566
|
-
* Use `getPostorder()` for a single canonical ordering.
|
|
2567
|
-
*
|
|
2568
|
-
* **O(V! · V)** worst-case (exponential).
|
|
2569
|
-
*
|
|
2570
|
-
* @example
|
|
2571
|
-
* ```ts
|
|
2572
|
-
* import { createGraph, genPostorders } from '@statelyai/graph';
|
|
2573
|
-
*
|
|
2574
|
-
* const graph = createGraph({
|
|
2575
|
-
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
2576
|
-
* edges: [
|
|
2577
|
-
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
2578
|
-
* { id: 'ac', sourceId: 'a', targetId: 'c' },
|
|
2579
|
-
* ],
|
|
2580
|
-
* initialNodeId: 'a',
|
|
2581
|
-
* });
|
|
2062
|
+
* Returns label-propagation communities for the graph.
|
|
2582
2063
|
*
|
|
2583
|
-
*
|
|
2584
|
-
*
|
|
2585
|
-
* // ['b', 'c', 'a'] or ['c', 'b', 'a']
|
|
2586
|
-
* }
|
|
2587
|
-
* ```
|
|
2064
|
+
* The implementation is deterministic: ties are broken by lexicographic label
|
|
2065
|
+
* order so test results remain stable.
|
|
2588
2066
|
*/
|
|
2589
|
-
function
|
|
2590
|
-
|
|
2591
|
-
const
|
|
2592
|
-
|
|
2593
|
-
const
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
let branched = false;
|
|
2603
|
-
while (dfsStack.length > 0) {
|
|
2604
|
-
const top = dfsStack[dfsStack.length - 1];
|
|
2605
|
-
const unvisited = getNeighborIds(graph, top).filter((id) => !visited.has(id));
|
|
2606
|
-
if (unvisited.length === 0) {
|
|
2607
|
-
dfsStack.pop();
|
|
2608
|
-
const ni = idx.nodeById.get(top);
|
|
2609
|
-
if (ni !== void 0) postorder = [...postorder, graph.nodes[ni]];
|
|
2610
|
-
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);
|
|
2611
2080
|
}
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
});
|
|
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;
|
|
2621
2089
|
}
|
|
2622
|
-
branched = true;
|
|
2623
|
-
break;
|
|
2624
2090
|
}
|
|
2625
|
-
|
|
2091
|
+
labels = nextLabels;
|
|
2092
|
+
if (!changed) break;
|
|
2626
2093
|
}
|
|
2094
|
+
return normalizeCommunities(graph, labels);
|
|
2627
2095
|
}
|
|
2628
2096
|
/**
|
|
2629
|
-
*
|
|
2630
|
-
*
|
|
2631
|
-
* from an arbitrary start node in directed graphs).
|
|
2632
|
-
*
|
|
2633
|
-
* **O(E log E)** using either edge sorting (Kruskal) or a min-heap (Prim).
|
|
2634
|
-
*
|
|
2635
|
-
* @example
|
|
2636
|
-
* ```ts
|
|
2637
|
-
* import { createGraph, getMinimumSpanningTree } from '@statelyai/graph';
|
|
2638
|
-
*
|
|
2639
|
-
* const graph = createGraph({
|
|
2640
|
-
* type: 'undirected',
|
|
2641
|
-
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
2642
|
-
* edges: [
|
|
2643
|
-
* { id: 'ab', sourceId: 'a', targetId: 'b', data: { weight: 1 } },
|
|
2644
|
-
* { id: 'bc', sourceId: 'b', targetId: 'c', data: { weight: 2 } },
|
|
2645
|
-
* { id: 'ac', sourceId: 'a', targetId: 'c', data: { weight: 3 } },
|
|
2646
|
-
* ],
|
|
2647
|
-
* });
|
|
2648
|
-
*
|
|
2649
|
-
* const mst = getMinimumSpanningTree(graph, {
|
|
2650
|
-
* getWeight: (e) => e.data.weight,
|
|
2651
|
-
* });
|
|
2652
|
-
* // mst has edges 'ab' and 'bc' (total weight 3)
|
|
2653
|
-
* ```
|
|
2097
|
+
* Lazily yields Girvan-Newman community splits as edge betweenness removes
|
|
2098
|
+
* bridge-like edges from the graph.
|
|
2654
2099
|
*/
|
|
2655
|
-
function
|
|
2656
|
-
|
|
2657
|
-
const
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
edges: mstEdges.map((e) => ({
|
|
2671
|
-
id: e.id,
|
|
2672
|
-
sourceId: e.sourceId,
|
|
2673
|
-
targetId: e.targetId,
|
|
2674
|
-
label: e.label,
|
|
2675
|
-
data: e.data,
|
|
2676
|
-
...e.weight !== void 0 && { weight: e.weight }
|
|
2677
|
-
}))
|
|
2678
|
-
});
|
|
2679
|
-
}
|
|
2680
|
-
function primMST(graph, getWeight) {
|
|
2681
|
-
if (graph.nodes.length === 0) return [];
|
|
2682
|
-
const idx = getIndex(graph);
|
|
2683
|
-
const inMST = /* @__PURE__ */ new Set();
|
|
2684
|
-
const mstEdges = [];
|
|
2685
|
-
const candidates = new MinPriorityQueue((a, b) => a.weight - b.weight);
|
|
2686
|
-
function addEdgesOf(nodeId) {
|
|
2687
|
-
for (const eid of idx.outEdges.get(nodeId) ?? []) {
|
|
2688
|
-
const ai = idx.edgeById.get(eid);
|
|
2689
|
-
if (ai === void 0) continue;
|
|
2690
|
-
const e = graph.edges[ai];
|
|
2691
|
-
if (!inMST.has(e.targetId)) candidates.push({
|
|
2692
|
-
weight: getWeight(e),
|
|
2693
|
-
edge: e
|
|
2694
|
-
});
|
|
2695
|
-
}
|
|
2696
|
-
if (graph.type === "undirected") for (const eid of idx.inEdges.get(nodeId) ?? []) {
|
|
2697
|
-
const ai = idx.edgeById.get(eid);
|
|
2698
|
-
if (ai === void 0) continue;
|
|
2699
|
-
const e = graph.edges[ai];
|
|
2700
|
-
if (!inMST.has(e.sourceId)) candidates.push({
|
|
2701
|
-
weight: getWeight(e),
|
|
2702
|
-
edge: e
|
|
2703
|
-
});
|
|
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;
|
|
2704
2115
|
}
|
|
2705
2116
|
}
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
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;
|
|
2716
2133
|
}
|
|
2717
|
-
return
|
|
2134
|
+
return last;
|
|
2718
2135
|
}
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
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;
|
|
2739
2160
|
}
|
|
2740
|
-
return true;
|
|
2741
2161
|
}
|
|
2742
|
-
|
|
2743
|
-
for (const e of sorted) if (union(e.sourceId, e.targetId)) mstEdges.push(e);
|
|
2744
|
-
return mstEdges;
|
|
2162
|
+
return modularity / m2;
|
|
2745
2163
|
}
|
|
2746
2164
|
/**
|
|
2747
|
-
* Returns
|
|
2748
|
-
*
|
|
2749
|
-
* Algorithm 'bellman-ford': handles negative weights, throws on negative cycles.
|
|
2750
|
-
* Algorithm 'floyd-warshall': classic dynamic programming.
|
|
2751
|
-
*
|
|
2752
|
-
* **O(V · (V + E) log V)** (Dijkstra), **O(V²E)** (Bellman-Ford), or **O(V³)** (Floyd-Warshall).
|
|
2753
|
-
*
|
|
2754
|
-
* @example
|
|
2755
|
-
* ```ts
|
|
2756
|
-
* import { createGraph, getAllPairsShortestPaths } from '@statelyai/graph';
|
|
2757
|
-
*
|
|
2758
|
-
* const graph = createGraph({
|
|
2759
|
-
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
2760
|
-
* edges: [
|
|
2761
|
-
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
2762
|
-
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
2763
|
-
* ],
|
|
2764
|
-
* });
|
|
2765
|
-
*
|
|
2766
|
-
* const allPaths = getAllPairsShortestPaths(graph);
|
|
2767
|
-
* // paths for every reachable (source, target) pair
|
|
2768
|
-
* ```
|
|
2165
|
+
* Returns communities found by greedily merging partitions that improve
|
|
2166
|
+
* modularity the most at each step.
|
|
2769
2167
|
*/
|
|
2770
|
-
function
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
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;
|
|
2775
2189
|
}
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
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
|
|
2783
2201
|
});
|
|
2784
|
-
results.push(...paths);
|
|
2785
2202
|
}
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
const paths = getShortestPaths(graph, {
|
|
2792
|
-
from: node.id,
|
|
2793
|
-
getWeight
|
|
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
|
|
2794
2208
|
});
|
|
2795
|
-
results.push(...paths);
|
|
2796
2209
|
}
|
|
2797
|
-
return
|
|
2210
|
+
return neighbors;
|
|
2798
2211
|
}
|
|
2799
|
-
function
|
|
2800
|
-
const
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
const dist = Array.from({ length: n }, () => Array(n).fill(INF));
|
|
2808
|
-
const prev = Array.from({ length: n }, () => Array.from({ length: n }, () => []));
|
|
2809
|
-
for (let i = 0; i < n; i++) dist[i][i] = 0;
|
|
2810
|
-
for (const e of graph.edges) {
|
|
2811
|
-
const u = idxOf.get(e.sourceId);
|
|
2812
|
-
const v = idxOf.get(e.targetId);
|
|
2813
|
-
const weight = w(e);
|
|
2814
|
-
if (weight < dist[u][v]) {
|
|
2815
|
-
dist[u][v] = weight;
|
|
2816
|
-
prev[u][v] = [{
|
|
2817
|
-
from: u,
|
|
2818
|
-
edge: e
|
|
2819
|
-
}];
|
|
2820
|
-
} else if (weight === dist[u][v] && weight < INF) prev[u][v].push({
|
|
2821
|
-
from: u,
|
|
2822
|
-
edge: e
|
|
2823
|
-
});
|
|
2824
|
-
if (graph.type === "undirected") {
|
|
2825
|
-
if (weight < dist[v][u]) {
|
|
2826
|
-
dist[v][u] = weight;
|
|
2827
|
-
prev[v][u] = [{
|
|
2828
|
-
from: v,
|
|
2829
|
-
edge: e
|
|
2830
|
-
}];
|
|
2831
|
-
} else if (weight === dist[v][u] && weight < INF) prev[v][u].push({
|
|
2832
|
-
from: v,
|
|
2833
|
-
edge: e
|
|
2834
|
-
});
|
|
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);
|
|
2835
2220
|
}
|
|
2221
|
+
if (edgeId === stopEdgeId) break;
|
|
2836
2222
|
}
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
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);
|
|
2845
2233
|
}
|
|
2846
2234
|
}
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
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)));
|
|
2856
2257
|
}
|
|
2857
2258
|
}
|
|
2858
|
-
|
|
2259
|
+
if (parentEdgeId === null && childCount > 1) state.articulationPoints.add(nodeId);
|
|
2859
2260
|
}
|
|
2860
|
-
function
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
edge,
|
|
2877
|
-
node: targetNode
|
|
2878
|
-
}]
|
|
2879
|
-
});
|
|
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);
|
|
2880
2277
|
}
|
|
2881
|
-
return
|
|
2278
|
+
return state;
|
|
2882
2279
|
}
|
|
2883
2280
|
/**
|
|
2884
|
-
* Returns
|
|
2885
|
-
* More efficient than Dijkstra when a good heuristic is available.
|
|
2886
|
-
*
|
|
2887
|
-
* **O((V + E) log V)** time with a good heuristic; degrades to Dijkstra
|
|
2888
|
-
* with `heuristic: () => 0`.
|
|
2281
|
+
* Returns bridge edges whose removal disconnects the graph.
|
|
2889
2282
|
*
|
|
2890
|
-
*
|
|
2891
|
-
|
|
2892
|
-
|
|
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.
|
|
2893
2292
|
*
|
|
2894
|
-
*
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
*
|
|
2903
|
-
* { id: 'ac', sourceId: 'a', targetId: 'c', weight: 3 },
|
|
2904
|
-
* ],
|
|
2905
|
-
* });
|
|
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.
|
|
2906
2302
|
*
|
|
2907
|
-
*
|
|
2908
|
-
* from: 'a',
|
|
2909
|
-
* to: 'c',
|
|
2910
|
-
* heuristic: (nodeId) => {
|
|
2911
|
-
* const node = graph.nodes.find(n => n.id === nodeId)!;
|
|
2912
|
-
* const target = graph.nodes.find(n => n.id === 'c')!;
|
|
2913
|
-
* return Math.abs(node.x! - target.x!) + Math.abs(node.y! - target.y!);
|
|
2914
|
-
* },
|
|
2915
|
-
* });
|
|
2916
|
-
* // path: a -> b -> c (weight 2, cheaper than direct a -> c)
|
|
2917
|
-
* ```
|
|
2303
|
+
* Articulation points may appear in multiple returned components.
|
|
2918
2304
|
*/
|
|
2919
|
-
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) {
|
|
2920
2314
|
const idx = getIndex(graph);
|
|
2921
|
-
const
|
|
2922
|
-
const
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
const
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
});
|
|
2939
|
-
while (openSet.size > 0) {
|
|
2940
|
-
const { id: currentId } = openSet.pop();
|
|
2941
|
-
if (closedSet.has(currentId)) continue;
|
|
2942
|
-
if (currentId === targetId) {
|
|
2943
|
-
const steps = [];
|
|
2944
|
-
let cur = targetId;
|
|
2945
|
-
while (cur !== sourceId) {
|
|
2946
|
-
const prev = cameFrom.get(cur);
|
|
2947
|
-
const ni = idx.nodeById.get(cur);
|
|
2948
|
-
steps.unshift({
|
|
2949
|
-
edge: prev.edge,
|
|
2950
|
-
node: graph.nodes[ni]
|
|
2951
|
-
});
|
|
2952
|
-
cur = prev.from;
|
|
2953
|
-
}
|
|
2954
|
-
return {
|
|
2955
|
-
source: graph.nodes[sourceNi],
|
|
2956
|
-
steps
|
|
2957
|
-
};
|
|
2958
|
-
}
|
|
2959
|
-
closedSet.add(currentId);
|
|
2960
|
-
for (const { neighborId, edge } of getNeighborEdges(graph, currentId)) {
|
|
2961
|
-
if (closedSet.has(neighborId)) continue;
|
|
2962
|
-
const tentativeG = (gScore.get(currentId) ?? Infinity) + getWeight(edge);
|
|
2963
|
-
if (tentativeG < (gScore.get(neighborId) ?? Infinity)) {
|
|
2964
|
-
cameFrom.set(neighborId, {
|
|
2965
|
-
from: currentId,
|
|
2966
|
-
edge
|
|
2967
|
-
});
|
|
2968
|
-
gScore.set(neighborId, tentativeG);
|
|
2969
|
-
openSet.push({
|
|
2970
|
-
id: neighborId,
|
|
2971
|
-
f: tentativeG + heuristic(neighborId)
|
|
2972
|
-
});
|
|
2973
|
-
}
|
|
2974
|
-
}
|
|
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);
|
|
2975
2332
|
}
|
|
2333
|
+
return true;
|
|
2976
2334
|
}
|
|
2977
2335
|
/**
|
|
2978
|
-
*
|
|
2979
|
-
* the source of the tail path (the overlap node).
|
|
2980
|
-
*
|
|
2981
|
-
* Steps are concatenated: head.steps ++ tail.steps (tail already starts
|
|
2982
|
-
* from the overlap node, so no slicing is needed).
|
|
2983
|
-
*
|
|
2984
|
-
* @example
|
|
2985
|
-
* ```ts
|
|
2986
|
-
* import { createGraph, getShortestPath, joinPaths } from '@statelyai/graph';
|
|
2987
|
-
*
|
|
2988
|
-
* const graph = createGraph({
|
|
2989
|
-
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
2990
|
-
* edges: [
|
|
2991
|
-
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
2992
|
-
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
2993
|
-
* ],
|
|
2994
|
-
* initialNodeId: 'a',
|
|
2995
|
-
* });
|
|
2336
|
+
* Returns whether two graphs are structurally isomorphic.
|
|
2996
2337
|
*
|
|
2997
|
-
*
|
|
2998
|
-
*
|
|
2999
|
-
* const ac = joinPaths(ab, bc);
|
|
3000
|
-
* // ac: a -> b -> c
|
|
3001
|
-
* ```
|
|
2338
|
+
* Optional `nodeMatch` and `edgeMatch` predicates can refine the match using
|
|
2339
|
+
* node and edge payloads.
|
|
3002
2340
|
*/
|
|
3003
|
-
function
|
|
3004
|
-
|
|
3005
|
-
if (
|
|
3006
|
-
return
|
|
3007
|
-
|
|
3008
|
-
|
|
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;
|
|
3009
2385
|
};
|
|
2386
|
+
return backtrack(0);
|
|
3010
2387
|
}
|
|
3011
2388
|
|
|
3012
2389
|
//#endregion
|
|
3013
|
-
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 };
|