@statelyai/graph 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -1
- package/dist/{algorithms-Pbj9dJB1.mjs → algorithms-3xvCxHzo.mjs} +724 -1
- package/dist/algorithms-g2uWmPrb.d.mts +786 -0
- package/dist/algorithms.d.mts +2 -647
- package/dist/algorithms.mjs +2 -2
- package/dist/index.d.mts +53 -2
- package/dist/index.mjs +94 -2
- package/package.json +4 -1
|
@@ -649,6 +649,729 @@ function collectDescendants(graph, id) {
|
|
|
649
649
|
return toDelete;
|
|
650
650
|
}
|
|
651
651
|
|
|
652
|
+
//#endregion
|
|
653
|
+
//#region src/algorithms/centrality.ts
|
|
654
|
+
function getNodeIds(graph) {
|
|
655
|
+
return graph.nodes.map((node) => node.id);
|
|
656
|
+
}
|
|
657
|
+
function getNeighborIds$1(graph, nodeId) {
|
|
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);
|
|
663
|
+
}
|
|
664
|
+
if (graph.type === "undirected") for (const edgeId of idx.inEdges.get(nodeId) ?? []) {
|
|
665
|
+
const edgeIndex = idx.edgeById.get(edgeId);
|
|
666
|
+
if (edgeIndex !== void 0) neighbors.push(graph.edges[edgeIndex].sourceId);
|
|
667
|
+
}
|
|
668
|
+
return neighbors;
|
|
669
|
+
}
|
|
670
|
+
function getIncomingIds(graph, nodeId) {
|
|
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);
|
|
676
|
+
}
|
|
677
|
+
if (graph.type === "undirected") for (const edgeId of idx.outEdges.get(nodeId) ?? []) {
|
|
678
|
+
const edgeIndex = idx.edgeById.get(edgeId);
|
|
679
|
+
if (edgeIndex !== void 0) incoming.push(graph.edges[edgeIndex].targetId);
|
|
680
|
+
}
|
|
681
|
+
return incoming;
|
|
682
|
+
}
|
|
683
|
+
function createEmptyScoreMap(graph) {
|
|
684
|
+
return Object.fromEntries(graph.nodes.map((node) => [node.id, 0]));
|
|
685
|
+
}
|
|
686
|
+
function normalizeVector(scores) {
|
|
687
|
+
const magnitude = Math.sqrt(Object.values(scores).reduce((sum, value) => sum + value * value, 0));
|
|
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);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return distances;
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Returns degree centrality scores for all nodes.
|
|
714
|
+
*
|
|
715
|
+
* Degree centrality is the node degree normalized by `n - 1`.
|
|
716
|
+
*
|
|
717
|
+
* @example
|
|
718
|
+
* ```ts
|
|
719
|
+
* const scores = getDegreeCentrality(graph);
|
|
720
|
+
* console.log(scores.a); // 0.5
|
|
721
|
+
* ```
|
|
722
|
+
*/
|
|
723
|
+
function getDegreeCentrality(graph) {
|
|
724
|
+
const scale = graph.nodes.length > 1 ? 1 / (graph.nodes.length - 1) : 0;
|
|
725
|
+
const idx = getIndex(graph);
|
|
726
|
+
const scores = createEmptyScoreMap(graph);
|
|
727
|
+
for (const node of graph.nodes) {
|
|
728
|
+
const outDegree = idx.outEdges.get(node.id)?.length ?? 0;
|
|
729
|
+
const inDegree = idx.inEdges.get(node.id)?.length ?? 0;
|
|
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;
|
|
732
|
+
}
|
|
733
|
+
return scores;
|
|
734
|
+
}
|
|
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;
|
|
742
|
+
const idx = getIndex(graph);
|
|
743
|
+
const scores = createEmptyScoreMap(graph);
|
|
744
|
+
for (const node of graph.nodes) scores[node.id] = (idx.inEdges.get(node.id)?.length ?? 0) * scale;
|
|
745
|
+
return scores;
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Returns out-degree centrality scores for all nodes.
|
|
749
|
+
*
|
|
750
|
+
* Out-degree centrality is the outgoing degree normalized by `n - 1`.
|
|
751
|
+
*/
|
|
752
|
+
function getOutDegreeCentrality(graph) {
|
|
753
|
+
const scale = graph.nodes.length > 1 ? 1 / (graph.nodes.length - 1) : 0;
|
|
754
|
+
const idx = getIndex(graph);
|
|
755
|
+
const scores = createEmptyScoreMap(graph);
|
|
756
|
+
for (const node of graph.nodes) scores[node.id] = (idx.outEdges.get(node.id)?.length ?? 0) * scale;
|
|
757
|
+
return scores;
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Returns closeness centrality scores for all nodes.
|
|
761
|
+
*
|
|
762
|
+
* Distances are computed over unweighted shortest paths using the graph's
|
|
763
|
+
* existing directed or undirected edge semantics.
|
|
764
|
+
*/
|
|
765
|
+
function getClosenessCentrality(graph) {
|
|
766
|
+
const scores = createEmptyScoreMap(graph);
|
|
767
|
+
const order = graph.nodes.length;
|
|
768
|
+
for (const node of graph.nodes) {
|
|
769
|
+
const distances = getReachableDistances(graph, node.id);
|
|
770
|
+
distances.delete(node.id);
|
|
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;
|
|
777
|
+
}
|
|
778
|
+
return scores;
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Returns betweenness centrality scores for all nodes.
|
|
782
|
+
*
|
|
783
|
+
* Uses Brandes' algorithm over unweighted shortest paths and returns
|
|
784
|
+
* normalized scores.
|
|
785
|
+
*/
|
|
786
|
+
function getBetweennessCentrality(graph) {
|
|
787
|
+
const scores = createEmptyScoreMap(graph);
|
|
788
|
+
for (const source of graph.nodes) {
|
|
789
|
+
const stack = [];
|
|
790
|
+
const predecessors = /* @__PURE__ */ new Map();
|
|
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);
|
|
798
|
+
}
|
|
799
|
+
sigma.set(source.id, 1);
|
|
800
|
+
distance.set(source.id, 0);
|
|
801
|
+
while (queue.length > 0) {
|
|
802
|
+
const currentId = queue.shift();
|
|
803
|
+
stack.push(currentId);
|
|
804
|
+
for (const neighborId of getNeighborIds$1(graph, currentId)) {
|
|
805
|
+
if (distance.get(neighborId) === -1) {
|
|
806
|
+
queue.push(neighborId);
|
|
807
|
+
distance.set(neighborId, distance.get(currentId) + 1);
|
|
808
|
+
}
|
|
809
|
+
if (distance.get(neighborId) === distance.get(currentId) + 1) {
|
|
810
|
+
sigma.set(neighborId, sigma.get(neighborId) + sigma.get(currentId));
|
|
811
|
+
predecessors.get(neighborId).push(currentId);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
const delta = /* @__PURE__ */ new Map();
|
|
816
|
+
for (const node of graph.nodes) delta.set(node.id, 0);
|
|
817
|
+
while (stack.length > 0) {
|
|
818
|
+
const nodeId = stack.pop();
|
|
819
|
+
const sigmaNode = sigma.get(nodeId);
|
|
820
|
+
if (sigmaNode === 0) continue;
|
|
821
|
+
for (const predecessorId of predecessors.get(nodeId)) {
|
|
822
|
+
const contribution = sigma.get(predecessorId) / sigmaNode * (1 + delta.get(nodeId));
|
|
823
|
+
delta.set(predecessorId, delta.get(predecessorId) + contribution);
|
|
824
|
+
}
|
|
825
|
+
if (nodeId !== source.id) scores[nodeId] += delta.get(nodeId);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
const order = graph.nodes.length;
|
|
829
|
+
if (order <= 2) return scores;
|
|
830
|
+
const scale = graph.type === "undirected" ? 1 / ((order - 1) * (order - 2) / 2) : 1 / ((order - 1) * (order - 2));
|
|
831
|
+
for (const nodeId of Object.keys(scores)) {
|
|
832
|
+
if (graph.type === "undirected") scores[nodeId] /= 2;
|
|
833
|
+
scores[nodeId] *= scale;
|
|
834
|
+
}
|
|
835
|
+
return scores;
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Returns PageRank scores for all nodes.
|
|
839
|
+
*
|
|
840
|
+
* Uses power iteration with damping factor `alpha`.
|
|
841
|
+
*/
|
|
842
|
+
function getPageRank(graph, options) {
|
|
843
|
+
const nodeIds = getNodeIds(graph);
|
|
844
|
+
if (nodeIds.length === 0) return {};
|
|
845
|
+
const alpha = options?.alpha ?? .85;
|
|
846
|
+
const maxIterations = options?.maxIterations ?? 100;
|
|
847
|
+
const tolerance = options?.tolerance ?? 1e-6;
|
|
848
|
+
let scores = Object.fromEntries(nodeIds.map((nodeId) => [nodeId, 1 / nodeIds.length]));
|
|
849
|
+
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
850
|
+
const nextScores = Object.fromEntries(nodeIds.map((nodeId) => [nodeId, (1 - alpha) / nodeIds.length]));
|
|
851
|
+
let danglingMass = 0;
|
|
852
|
+
for (const nodeId of nodeIds) {
|
|
853
|
+
const neighbors = getNeighborIds$1(graph, nodeId);
|
|
854
|
+
if (neighbors.length === 0) {
|
|
855
|
+
danglingMass += scores[nodeId];
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
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
|
+
}
|
|
869
|
+
scores = nextScores;
|
|
870
|
+
}
|
|
871
|
+
const total = Object.values(scores).reduce((sum, value) => sum + value, 0);
|
|
872
|
+
if (total !== 0) for (const nodeId of nodeIds) scores[nodeId] /= total;
|
|
873
|
+
return scores;
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Returns HITS hub and authority scores for all nodes.
|
|
877
|
+
*
|
|
878
|
+
* Uses power iteration and L2 normalization per iteration.
|
|
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;
|
|
902
|
+
}
|
|
903
|
+
return {
|
|
904
|
+
hubs,
|
|
905
|
+
authorities
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* Returns eigenvector centrality scores for all nodes.
|
|
910
|
+
*
|
|
911
|
+
* Uses power iteration over incoming neighbors for directed graphs and
|
|
912
|
+
* undirected adjacency for undirected graphs.
|
|
913
|
+
*/
|
|
914
|
+
function getEigenvectorCentrality(graph, options) {
|
|
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;
|
|
928
|
+
}
|
|
929
|
+
return scores;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
//#endregion
|
|
933
|
+
//#region src/algorithms/community.ts
|
|
934
|
+
function getUndirectedNeighbors$1(graph, nodeId) {
|
|
935
|
+
const idx = getIndex(graph);
|
|
936
|
+
const neighbors = [];
|
|
937
|
+
for (const edgeId of idx.outEdges.get(nodeId) ?? []) {
|
|
938
|
+
const edgeIndex = idx.edgeById.get(edgeId);
|
|
939
|
+
if (edgeIndex !== void 0) neighbors.push({
|
|
940
|
+
nodeId: graph.edges[edgeIndex].targetId,
|
|
941
|
+
edgeId
|
|
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;
|
|
952
|
+
}
|
|
953
|
+
function getUndirectedConnectedComponents(graph) {
|
|
954
|
+
const idx = getIndex(graph);
|
|
955
|
+
const visited = /* @__PURE__ */ new Set();
|
|
956
|
+
const communities = [];
|
|
957
|
+
for (const node of graph.nodes) {
|
|
958
|
+
if (visited.has(node.id)) continue;
|
|
959
|
+
const community = [];
|
|
960
|
+
const queue = [node.id];
|
|
961
|
+
visited.add(node.id);
|
|
962
|
+
while (queue.length > 0) {
|
|
963
|
+
const currentId = queue.shift();
|
|
964
|
+
const nodeIndex = idx.nodeById.get(currentId);
|
|
965
|
+
if (nodeIndex !== void 0) community.push(graph.nodes[nodeIndex]);
|
|
966
|
+
for (const neighbor of getUndirectedNeighbors$1(graph, currentId)) {
|
|
967
|
+
if (visited.has(neighbor.nodeId)) continue;
|
|
968
|
+
visited.add(neighbor.nodeId);
|
|
969
|
+
queue.push(neighbor.nodeId);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
communities.push(community.sort((a, b) => a.id.localeCompare(b.id)));
|
|
973
|
+
}
|
|
974
|
+
return communities.sort((a, b) => a[0].id.localeCompare(b[0].id));
|
|
975
|
+
}
|
|
976
|
+
function getNodeMap(graph) {
|
|
977
|
+
return new Map(graph.nodes.map((node) => [node.id, node]));
|
|
978
|
+
}
|
|
979
|
+
function normalizeCommunities(graph, labels) {
|
|
980
|
+
const nodeMap = getNodeMap(graph);
|
|
981
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
982
|
+
for (const [nodeId, label] of Object.entries(labels)) {
|
|
983
|
+
if (!grouped.has(label)) grouped.set(label, []);
|
|
984
|
+
const node = nodeMap.get(nodeId);
|
|
985
|
+
if (node) grouped.get(label).push(node);
|
|
986
|
+
}
|
|
987
|
+
return [...grouped.values()].map((community) => community.sort((a, b) => a.id.localeCompare(b.id))).sort((a, b) => a[0].id.localeCompare(b[0].id));
|
|
988
|
+
}
|
|
989
|
+
function getEdgeBetweenness(graph) {
|
|
990
|
+
const scores = Object.fromEntries(graph.edges.map((edge) => [edge.id, 0]));
|
|
991
|
+
for (const source of graph.nodes) {
|
|
992
|
+
const stack = [];
|
|
993
|
+
const predecessors = /* @__PURE__ */ new Map();
|
|
994
|
+
const sigma = /* @__PURE__ */ new Map();
|
|
995
|
+
const distance = /* @__PURE__ */ new Map();
|
|
996
|
+
const queue = [source.id];
|
|
997
|
+
for (const node of graph.nodes) {
|
|
998
|
+
predecessors.set(node.id, []);
|
|
999
|
+
sigma.set(node.id, 0);
|
|
1000
|
+
distance.set(node.id, -1);
|
|
1001
|
+
}
|
|
1002
|
+
sigma.set(source.id, 1);
|
|
1003
|
+
distance.set(source.id, 0);
|
|
1004
|
+
while (queue.length > 0) {
|
|
1005
|
+
const currentId = queue.shift();
|
|
1006
|
+
stack.push(currentId);
|
|
1007
|
+
for (const neighbor of getUndirectedNeighbors$1(graph, currentId)) {
|
|
1008
|
+
if (distance.get(neighbor.nodeId) === -1) {
|
|
1009
|
+
queue.push(neighbor.nodeId);
|
|
1010
|
+
distance.set(neighbor.nodeId, distance.get(currentId) + 1);
|
|
1011
|
+
}
|
|
1012
|
+
if (distance.get(neighbor.nodeId) === distance.get(currentId) + 1) {
|
|
1013
|
+
sigma.set(neighbor.nodeId, sigma.get(neighbor.nodeId) + sigma.get(currentId));
|
|
1014
|
+
predecessors.get(neighbor.nodeId).push({
|
|
1015
|
+
nodeId: currentId,
|
|
1016
|
+
edgeId: neighbor.edgeId
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
const delta = /* @__PURE__ */ new Map();
|
|
1022
|
+
for (const node of graph.nodes) delta.set(node.id, 0);
|
|
1023
|
+
while (stack.length > 0) {
|
|
1024
|
+
const nodeId = stack.pop();
|
|
1025
|
+
const sigmaNode = sigma.get(nodeId);
|
|
1026
|
+
if (sigmaNode === 0) continue;
|
|
1027
|
+
for (const predecessor of predecessors.get(nodeId)) {
|
|
1028
|
+
const contribution = sigma.get(predecessor.nodeId) / sigmaNode * (1 + delta.get(nodeId));
|
|
1029
|
+
scores[predecessor.edgeId] += contribution;
|
|
1030
|
+
delta.set(predecessor.nodeId, delta.get(predecessor.nodeId) + contribution);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
for (const edgeId of Object.keys(scores)) scores[edgeId] /= 2;
|
|
1035
|
+
return scores;
|
|
1036
|
+
}
|
|
1037
|
+
function cloneWithEdges(graph, edges) {
|
|
1038
|
+
return {
|
|
1039
|
+
...graph,
|
|
1040
|
+
nodes: [...graph.nodes],
|
|
1041
|
+
edges
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
function toCommunityIds(communities) {
|
|
1045
|
+
return communities.map((community) => new Set(community.map((node) => node.id)));
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Returns label-propagation communities for the graph.
|
|
1049
|
+
*
|
|
1050
|
+
* The implementation is deterministic: ties are broken by lexicographic label
|
|
1051
|
+
* order so test results remain stable.
|
|
1052
|
+
*/
|
|
1053
|
+
function getLabelPropagationCommunities(graph, options) {
|
|
1054
|
+
if (graph.nodes.length === 0) return [];
|
|
1055
|
+
const maxIterations = options?.maxIterations ?? 50;
|
|
1056
|
+
let labels = Object.fromEntries(graph.nodes.map((node) => [node.id, node.id]));
|
|
1057
|
+
const nodeIds = graph.nodes.map((node) => node.id).sort();
|
|
1058
|
+
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
1059
|
+
const nextLabels = { ...labels };
|
|
1060
|
+
let changed = false;
|
|
1061
|
+
for (const nodeId of nodeIds) {
|
|
1062
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1063
|
+
for (const neighbor of getUndirectedNeighbors$1(graph, nodeId)) {
|
|
1064
|
+
const label = labels[neighbor.nodeId];
|
|
1065
|
+
counts.set(label, (counts.get(label) ?? 0) + 1);
|
|
1066
|
+
}
|
|
1067
|
+
if (counts.size === 0) continue;
|
|
1068
|
+
const bestLabel = [...counts.entries()].sort((a, b) => {
|
|
1069
|
+
if (b[1] !== a[1]) return b[1] - a[1];
|
|
1070
|
+
return a[0].localeCompare(b[0]);
|
|
1071
|
+
})[0][0];
|
|
1072
|
+
if (bestLabel !== labels[nodeId]) {
|
|
1073
|
+
nextLabels[nodeId] = bestLabel;
|
|
1074
|
+
changed = true;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
labels = nextLabels;
|
|
1078
|
+
if (!changed) break;
|
|
1079
|
+
}
|
|
1080
|
+
return normalizeCommunities(graph, labels);
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Lazily yields Girvan-Newman community splits as edge betweenness removes
|
|
1084
|
+
* bridge-like edges from the graph.
|
|
1085
|
+
*/
|
|
1086
|
+
function* genGirvanNewmanCommunities(graph, options) {
|
|
1087
|
+
if (graph.nodes.length === 0 || graph.edges.length === 0) return;
|
|
1088
|
+
const maxLevels = options?.maxLevels ?? Number.POSITIVE_INFINITY;
|
|
1089
|
+
let yielded = 0;
|
|
1090
|
+
let edges = [...graph.edges];
|
|
1091
|
+
let previousCount = getUndirectedConnectedComponents(graph).length;
|
|
1092
|
+
while (edges.length > 0 && yielded < maxLevels) {
|
|
1093
|
+
const betweenness = getEdgeBetweenness(cloneWithEdges(graph, edges));
|
|
1094
|
+
const maxScore = Math.max(...Object.values(betweenness));
|
|
1095
|
+
edges = edges.filter((edge) => betweenness[edge.id] < maxScore - 1e-12);
|
|
1096
|
+
const components = getUndirectedConnectedComponents(cloneWithEdges(graph, edges));
|
|
1097
|
+
if (components.length > previousCount) {
|
|
1098
|
+
yield components;
|
|
1099
|
+
yielded++;
|
|
1100
|
+
previousCount = components.length;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
/**
|
|
1105
|
+
* Returns the requested Girvan-Newman split level eagerly.
|
|
1106
|
+
*
|
|
1107
|
+
* `level: 1` returns the first split yielded by `genGirvanNewmanCommunities`.
|
|
1108
|
+
*/
|
|
1109
|
+
function getGirvanNewmanCommunities(graph, options) {
|
|
1110
|
+
if (graph.nodes.length === 0) return [];
|
|
1111
|
+
const targetLevel = options?.level ?? 1;
|
|
1112
|
+
if (targetLevel <= 0) return getUndirectedConnectedComponents(graph);
|
|
1113
|
+
let last = getUndirectedConnectedComponents(graph);
|
|
1114
|
+
let level = 0;
|
|
1115
|
+
for (const partition of genGirvanNewmanCommunities(graph, { maxLevels: targetLevel })) {
|
|
1116
|
+
last = partition;
|
|
1117
|
+
level++;
|
|
1118
|
+
if (level >= targetLevel) break;
|
|
1119
|
+
}
|
|
1120
|
+
return last;
|
|
1121
|
+
}
|
|
1122
|
+
/**
|
|
1123
|
+
* Returns the modularity score for a partition of communities.
|
|
1124
|
+
*
|
|
1125
|
+
* Community algorithms in this module treat the graph as undirected.
|
|
1126
|
+
*/
|
|
1127
|
+
function getModularity(graph, communities) {
|
|
1128
|
+
if (graph.edges.length === 0 || communities.length === 0) return 0;
|
|
1129
|
+
const nodeIds = graph.nodes.map((node) => node.id);
|
|
1130
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
1131
|
+
const degree = Object.fromEntries(nodeIds.map((nodeId) => [nodeId, 0]));
|
|
1132
|
+
for (const nodeId of nodeIds) adjacency.set(nodeId, /* @__PURE__ */ new Map());
|
|
1133
|
+
for (const edge of graph.edges) {
|
|
1134
|
+
adjacency.get(edge.sourceId).set(edge.targetId, (adjacency.get(edge.sourceId).get(edge.targetId) ?? 0) + 1);
|
|
1135
|
+
adjacency.get(edge.targetId).set(edge.sourceId, (adjacency.get(edge.targetId).get(edge.sourceId) ?? 0) + 1);
|
|
1136
|
+
degree[edge.sourceId]++;
|
|
1137
|
+
degree[edge.targetId]++;
|
|
1138
|
+
}
|
|
1139
|
+
const m2 = graph.edges.length * 2;
|
|
1140
|
+
let modularity = 0;
|
|
1141
|
+
for (const community of toCommunityIds(communities)) {
|
|
1142
|
+
const ids = [...community];
|
|
1143
|
+
for (const i of ids) for (const j of ids) {
|
|
1144
|
+
const aij = adjacency.get(i).get(j) ?? 0;
|
|
1145
|
+
modularity += aij - degree[i] * degree[j] / m2;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
return modularity / m2;
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Returns communities found by greedily merging partitions that improve
|
|
1152
|
+
* modularity the most at each step.
|
|
1153
|
+
*/
|
|
1154
|
+
function getGreedyModularityCommunities(graph) {
|
|
1155
|
+
if (graph.nodes.length === 0) return [];
|
|
1156
|
+
let communities = graph.nodes.map((node) => [node]);
|
|
1157
|
+
let currentScore = getModularity(graph, communities);
|
|
1158
|
+
while (communities.length > 1) {
|
|
1159
|
+
let bestScore = currentScore;
|
|
1160
|
+
let bestMerge;
|
|
1161
|
+
for (let i = 0; i < communities.length; i++) for (let j = i + 1; j < communities.length; j++) {
|
|
1162
|
+
const merged = communities.filter((_, index) => index !== i && index !== j);
|
|
1163
|
+
merged.push([...communities[i], ...communities[j]].sort((a, b) => a.id.localeCompare(b.id)));
|
|
1164
|
+
const score = getModularity(graph, merged);
|
|
1165
|
+
if (score > bestScore + 1e-12) {
|
|
1166
|
+
bestScore = score;
|
|
1167
|
+
bestMerge = merged;
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
if (!bestMerge) break;
|
|
1171
|
+
communities = bestMerge.sort((a, b) => a[0].id.localeCompare(b[0].id));
|
|
1172
|
+
currentScore = bestScore;
|
|
1173
|
+
}
|
|
1174
|
+
return communities;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
//#endregion
|
|
1178
|
+
//#region src/algorithms/connectivity.ts
|
|
1179
|
+
function getUndirectedNeighbors(graph, nodeId) {
|
|
1180
|
+
const idx = getIndex(graph);
|
|
1181
|
+
const neighbors = [];
|
|
1182
|
+
for (const edgeId of idx.outEdges.get(nodeId) ?? []) {
|
|
1183
|
+
const edgeIndex = idx.edgeById.get(edgeId);
|
|
1184
|
+
if (edgeIndex !== void 0) neighbors.push({
|
|
1185
|
+
nodeId: graph.edges[edgeIndex].targetId,
|
|
1186
|
+
edgeId
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
for (const edgeId of idx.inEdges.get(nodeId) ?? []) {
|
|
1190
|
+
const edgeIndex = idx.edgeById.get(edgeId);
|
|
1191
|
+
if (edgeIndex !== void 0) neighbors.push({
|
|
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);
|
|
1206
|
+
}
|
|
1207
|
+
if (edgeId === stopEdgeId) break;
|
|
1208
|
+
}
|
|
1209
|
+
if (nodeIds.size > 0) state.components.push(nodeIds);
|
|
1210
|
+
}
|
|
1211
|
+
function finalizeRemainingComponent(state) {
|
|
1212
|
+
if (state.edgeStack.length === 0) return;
|
|
1213
|
+
const nodeIds = /* @__PURE__ */ new Set();
|
|
1214
|
+
while (state.edgeStack.length > 0) {
|
|
1215
|
+
const edge = state.edgeById.get(state.edgeStack.pop());
|
|
1216
|
+
if (edge) {
|
|
1217
|
+
nodeIds.add(edge.sourceId);
|
|
1218
|
+
nodeIds.add(edge.targetId);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
if (nodeIds.size > 0) state.components.push(nodeIds);
|
|
1222
|
+
}
|
|
1223
|
+
function traverseConnectivity(graph, nodeId, parentEdgeId, state) {
|
|
1224
|
+
state.time += 1;
|
|
1225
|
+
state.disc.set(nodeId, state.time);
|
|
1226
|
+
state.low.set(nodeId, state.time);
|
|
1227
|
+
let childCount = 0;
|
|
1228
|
+
for (const neighbor of getUndirectedNeighbors(graph, nodeId)) {
|
|
1229
|
+
if (neighbor.edgeId === parentEdgeId) continue;
|
|
1230
|
+
if (!state.disc.has(neighbor.nodeId)) {
|
|
1231
|
+
childCount += 1;
|
|
1232
|
+
state.edgeStack.push(neighbor.edgeId);
|
|
1233
|
+
traverseConnectivity(graph, neighbor.nodeId, neighbor.edgeId, state);
|
|
1234
|
+
state.low.set(nodeId, Math.min(state.low.get(nodeId), state.low.get(neighbor.nodeId)));
|
|
1235
|
+
if (state.low.get(neighbor.nodeId) > state.disc.get(nodeId)) state.bridges.add(neighbor.edgeId);
|
|
1236
|
+
if (parentEdgeId !== null && state.low.get(neighbor.nodeId) >= state.disc.get(nodeId)) {
|
|
1237
|
+
state.articulationPoints.add(nodeId);
|
|
1238
|
+
popComponentUntil(state, neighbor.edgeId);
|
|
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)));
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
if (parentEdgeId === null && childCount > 1) state.articulationPoints.add(nodeId);
|
|
1246
|
+
}
|
|
1247
|
+
function analyzeConnectivity(graph) {
|
|
1248
|
+
const state = {
|
|
1249
|
+
time: 0,
|
|
1250
|
+
disc: /* @__PURE__ */ new Map(),
|
|
1251
|
+
low: /* @__PURE__ */ new Map(),
|
|
1252
|
+
edgeStack: [],
|
|
1253
|
+
bridges: /* @__PURE__ */ new Set(),
|
|
1254
|
+
articulationPoints: /* @__PURE__ */ new Set(),
|
|
1255
|
+
components: [],
|
|
1256
|
+
nodeById: new Map(graph.nodes.map((node) => [node.id, node])),
|
|
1257
|
+
edgeById: new Map(graph.edges.map((edge) => [edge.id, edge]))
|
|
1258
|
+
};
|
|
1259
|
+
for (const node of graph.nodes) {
|
|
1260
|
+
if (state.disc.has(node.id)) continue;
|
|
1261
|
+
traverseConnectivity(graph, node.id, null, state);
|
|
1262
|
+
finalizeRemainingComponent(state);
|
|
1263
|
+
}
|
|
1264
|
+
return state;
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Returns bridge edges whose removal disconnects the graph.
|
|
1268
|
+
*
|
|
1269
|
+
* Connectivity algorithms in this module treat the graph as undirected.
|
|
1270
|
+
*/
|
|
1271
|
+
function getBridges(graph) {
|
|
1272
|
+
if (graph.edges.length === 0) return [];
|
|
1273
|
+
const state = analyzeConnectivity(graph);
|
|
1274
|
+
return [...state.bridges].map((edgeId) => state.edgeById.get(edgeId)).sort((a, b) => a.id.localeCompare(b.id));
|
|
1275
|
+
}
|
|
1276
|
+
/**
|
|
1277
|
+
* Returns articulation points (cut vertices) for the graph.
|
|
1278
|
+
*
|
|
1279
|
+
* Connectivity algorithms in this module treat the graph as undirected.
|
|
1280
|
+
*/
|
|
1281
|
+
function getArticulationPoints(graph) {
|
|
1282
|
+
if (graph.nodes.length === 0) return [];
|
|
1283
|
+
const state = analyzeConnectivity(graph);
|
|
1284
|
+
return [...state.articulationPoints].map((nodeId) => state.nodeById.get(nodeId)).sort((a, b) => a.id.localeCompare(b.id));
|
|
1285
|
+
}
|
|
1286
|
+
/**
|
|
1287
|
+
* Returns biconnected components as arrays of nodes.
|
|
1288
|
+
*
|
|
1289
|
+
* Articulation points may appear in multiple returned components.
|
|
1290
|
+
*/
|
|
1291
|
+
function getBiconnectedComponents(graph) {
|
|
1292
|
+
if (graph.edges.length === 0) return [];
|
|
1293
|
+
const state = analyzeConnectivity(graph);
|
|
1294
|
+
return state.components.map((component) => [...component].map((nodeId) => state.nodeById.get(nodeId)).sort((a, b) => a.id.localeCompare(b.id))).sort((a, b) => a[0].id.localeCompare(b[0].id));
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
//#endregion
|
|
1298
|
+
//#region src/algorithms/isomorphism.ts
|
|
1299
|
+
function getDegreeSignature(graph, nodeId) {
|
|
1300
|
+
const idx = getIndex(graph);
|
|
1301
|
+
const outDegree = idx.outEdges.get(nodeId)?.length ?? 0;
|
|
1302
|
+
const inDegree = idx.inEdges.get(nodeId)?.length ?? 0;
|
|
1303
|
+
if (graph.type === "undirected") return `u:${new Set([...idx.outEdges.get(nodeId) ?? [], ...idx.inEdges.get(nodeId) ?? []]).size}`;
|
|
1304
|
+
return `d:${inDegree}:${outDegree}`;
|
|
1305
|
+
}
|
|
1306
|
+
function getEdgesBetween(graph, sourceId, targetId) {
|
|
1307
|
+
if (graph.type === "undirected") return graph.edges.filter((edge) => edge.sourceId === sourceId && edge.targetId === targetId || edge.sourceId === targetId && edge.targetId === sourceId);
|
|
1308
|
+
return graph.edges.filter((edge) => edge.sourceId === sourceId && edge.targetId === targetId);
|
|
1309
|
+
}
|
|
1310
|
+
function edgesAreCompatible(edgesA, edgesB, edgeMatch) {
|
|
1311
|
+
if (edgesA.length !== edgesB.length) return false;
|
|
1312
|
+
if (!edgeMatch || edgesA.length === 0) return true;
|
|
1313
|
+
const remaining = [...edgesB];
|
|
1314
|
+
for (const edgeA of edgesA) {
|
|
1315
|
+
const matchIndex = remaining.findIndex((edgeB) => edgeMatch(edgeA, edgeB));
|
|
1316
|
+
if (matchIndex === -1) return false;
|
|
1317
|
+
remaining.splice(matchIndex, 1);
|
|
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;
|
|
1357
|
+
}
|
|
1358
|
+
if (!edgesAreCompatible(getEdgesBetween(graphA, mappedAId, nodeA.id), getEdgesBetween(graphB, mappedBId, nodeB.id), edgeMatch)) {
|
|
1359
|
+
compatible = false;
|
|
1360
|
+
break;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
if (!compatible) continue;
|
|
1364
|
+
mapping.set(nodeA.id, nodeB.id);
|
|
1365
|
+
usedB.add(nodeB.id);
|
|
1366
|
+
if (backtrack(index + 1)) return true;
|
|
1367
|
+
mapping.delete(nodeA.id);
|
|
1368
|
+
usedB.delete(nodeB.id);
|
|
1369
|
+
}
|
|
1370
|
+
return false;
|
|
1371
|
+
};
|
|
1372
|
+
return backtrack(0);
|
|
1373
|
+
}
|
|
1374
|
+
|
|
652
1375
|
//#endregion
|
|
653
1376
|
//#region src/algorithms.ts
|
|
654
1377
|
/**
|
|
@@ -2204,4 +2927,4 @@ function joinPaths(headPath, tailPath) {
|
|
|
2204
2927
|
}
|
|
2205
2928
|
|
|
2206
2929
|
//#endregion
|
|
2207
|
-
export {
|
|
2930
|
+
export { createGraphPort as $, getBiconnectedComponents as A, getEigenvectorCentrality as B, hasPath as C, joinPaths as D, isTree as E, getLabelPropagationCommunities as F, GraphInstance as G, getInDegreeCentrality as H, getModularity as I, addNode as J, addEdge as K, getBetweennessCentrality as L, genGirvanNewmanCommunities as M, getGirvanNewmanCommunities as N, isIsomorphic as O, getGreedyModularityCommunities as P, createGraphNode as Q, getClosenessCentrality as R, getTopologicalSort as S, isConnected as T, getOutDegreeCentrality as U, getHITS as V, getPageRank as W, createGraphEdge as X, createGraph as Y, createGraphFromTransition as Z, getShortestPath as _, genPreorders as a, getNode as at, getSimplePaths as b, getAStarPath as c, updateEdge as ct, getCycles as d, createVisualGraph as et, getMinimumSpanningTree as f, getPreorders as g, getPreorder as h, genPostorders as i, getEdge as it, getBridges as j, getArticulationPoints as k, getAllPairsShortestPaths as l, updateEntities as lt, getPostorders as m, dfs as n, deleteEntities as nt, genShortestPaths as o, hasEdge as ot, getPostorder as p, addEntities as q, genCycles as r, deleteNode as rt, genSimplePaths as s, hasNode as st, bfs as t, deleteEdge as tt, getConnectedComponents as u, updateNode as ut, getShortestPaths as v, isAcyclic as w, getStronglyConnectedComponents as x, getSimplePath as y, getDegreeCentrality as z };
|