@statelyai/graph 0.10.0 → 0.11.1

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.
Files changed (49) hide show
  1. package/README.md +26 -6
  2. package/dist/{algorithms-C-S7u40k.mjs → algorithms-BHHg7lGq.mjs} +1509 -2132
  3. package/dist/algorithms-BlM-qoJb.d.mts +178 -0
  4. package/dist/algorithms.d.mts +1 -1
  5. package/dist/algorithms.mjs +1 -1
  6. package/dist/{converter-B5CUD0r9.mjs → converter-Dspillnn.mjs} +2 -2
  7. package/dist/format-support.d.mts +22 -0
  8. package/dist/format-support.mjs +309 -0
  9. package/dist/formats/adjacency-list/index.d.mts +1 -1
  10. package/dist/formats/adjacency-list/index.mjs +1 -1
  11. package/dist/formats/converter/index.d.mts +1 -1
  12. package/dist/formats/converter/index.mjs +1 -1
  13. package/dist/formats/cytoscape/index.d.mts +1 -1
  14. package/dist/formats/cytoscape/index.mjs +1 -1
  15. package/dist/formats/d3/index.d.mts +1 -1
  16. package/dist/formats/d3/index.mjs +1 -1
  17. package/dist/formats/dot/index.d.mts +1 -1
  18. package/dist/formats/dot/index.mjs +1 -1
  19. package/dist/formats/edge-list/index.d.mts +1 -1
  20. package/dist/formats/edge-list/index.mjs +1 -1
  21. package/dist/formats/elk/index.d.mts +1 -1
  22. package/dist/formats/gexf/index.d.mts +1 -1
  23. package/dist/formats/gexf/index.mjs +1 -1
  24. package/dist/formats/gml/index.d.mts +1 -1
  25. package/dist/formats/gml/index.mjs +1 -1
  26. package/dist/formats/graphml/index.d.mts +1 -1
  27. package/dist/formats/graphml/index.mjs +34 -1
  28. package/dist/formats/jgf/index.d.mts +1 -1
  29. package/dist/formats/jgf/index.mjs +1 -1
  30. package/dist/formats/mermaid/index.d.mts +47 -3
  31. package/dist/formats/mermaid/index.mjs +234 -29
  32. package/dist/formats/tgf/index.d.mts +1 -1
  33. package/dist/formats/tgf/index.mjs +1 -1
  34. package/dist/formats/xyflow/index.d.mts +1 -1
  35. package/dist/index.d.mts +2 -2
  36. package/dist/index.mjs +3 -3
  37. package/dist/queries.d.mts +1 -1
  38. package/dist/queries.mjs +1 -1
  39. package/dist/schemas.d.mts +52 -3
  40. package/dist/schemas.mjs +22 -3
  41. package/package.json +6 -2
  42. package/schemas/edge.schema.json +16 -1
  43. package/schemas/graph.schema.json +80 -4
  44. package/schemas/node.schema.json +64 -2
  45. package/dist/algorithms-DdjFO-ft.d.mts +0 -787
  46. /package/dist/{adjacency-list-fldj-QAL.mjs → adjacency-list-Ca0VjKIf.mjs} +0 -0
  47. /package/dist/{edge-list-Br05wXMg.mjs → edge-list-gKe8-iRa.mjs} +0 -0
  48. /package/dist/{indexing-DyfgLuzw.mjs → indexing-CJc-ul8e.mjs} +0 -0
  49. /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-DyfgLuzw.mjs";
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/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);
653
+ //#region src/algorithms/shared.ts
654
+ var MinPriorityQueue = class {
655
+ items = [];
656
+ constructor(compare) {
657
+ this.compare = compare;
663
658
  }
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);
659
+ get size() {
660
+ return this.items.length;
667
661
  }
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);
662
+ push(item) {
663
+ this.items.push(item);
664
+ this.bubbleUp(this.items.length - 1);
676
665
  }
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);
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
- 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);
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
- 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;
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 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;
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
- return scores;
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
- 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;
714
+ return (idx.outEdges.get(nodeId) ?? []).map((eid) => graph.edges[idx.edgeById.get(eid)].targetId);
746
715
  }
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;
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
- * 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;
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
- return scores;
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
- * 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);
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
- sigma.set(source.id, 1);
800
- distance.set(source.id, 0);
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 currentId = queue.shift();
803
- stack.push(currentId);
804
- for (const neighborId of getNeighborIds$1(graph, currentId)) {
805
- if (distance.get(neighborId) === -1) {
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
- 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
- }
800
+ } else if (existing === nextDistance) prev.get(neighborId).push({
801
+ from: id,
802
+ edge
803
+ });
813
804
  }
814
805
  }
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);
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
- 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;
838
+ return {
839
+ dist,
840
+ prev
841
+ };
836
842
  }
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;
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
- scores = nextScores;
888
+ if (!changed) break;
870
889
  }
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;
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
- hubs,
905
- authorities
900
+ dist,
901
+ prev
906
902
  };
907
903
  }
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;
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
- return scores;
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 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;
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 getUndirectedConnectedComponents(graph) {
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 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);
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
- communities.push(community.sort((a, b) => a.id.localeCompare(b.id)));
975
+ visited.delete(nodeId);
973
976
  }
974
- return communities.sort((a, b) => a[0].id.localeCompare(b[0].id));
977
+ yield* dfsCollect(sourceId);
975
978
  }
976
- function getNodeMap(graph) {
977
- return new Map(graph.nodes.map((node) => [node.id, node]));
979
+ function getSimplePath(graph, opts) {
980
+ for (const path of genSimplePaths(graph, opts)) return path;
978
981
  }
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);
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
- return [...grouped.values()].map((community) => community.sort((a, b) => a.id.localeCompare(b.id))).sort((a, b) => a[0].id.localeCompare(b[0].id));
1017
+ for (const node of graph.nodes) if (!nodeIndex.has(node.id)) strongconnect(node.id);
1018
+ return result;
988
1019
  }
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
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
- 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);
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 cloneWithEdges(graph, edges) {
1038
- return {
1039
- ...graph,
1040
- nodes: [...graph.nodes],
1041
- edges
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 toCommunityIds(communities) {
1045
- return communities.map((community) => new Set(community.map((node) => node.id)));
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
- * 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
- }
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
- 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;
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
- * 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;
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 last;
1219
+ return results;
1121
1220
  }
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;
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
- 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;
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
- return communities;
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/connectivity.ts
1179
- function getUndirectedNeighbors(graph, nodeId) {
1289
+ //#region src/algorithms/traversal.ts
1290
+ function* bfs(graph, startId) {
1180
1291
  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);
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 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
- }
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 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)));
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
- if (parentEdgeId === null && childCount > 1) state.articulationPoints.add(nodeId);
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 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]))
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
- if (state.disc.has(node.id)) continue;
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 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;
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
- if (!edgesAreCompatible(getEdgesBetween(graphA, mappedAId, nodeA.id), getEdgesBetween(graphB, mappedBId, nodeB.id), edgeMatch)) {
1359
- compatible = false;
1360
- break;
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
- return false;
1371
- };
1372
- return backtrack(0);
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 visited = /* @__PURE__ */ new Set();
1399
- const queue = [startId];
1400
- visited.add(startId);
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 === void 0) continue;
1405
- yield graph.nodes[ni];
1406
- for (const nId of getNeighborIds(graph, id)) if (!visited.has(nId)) {
1407
- visited.add(nId);
1408
- queue.push(nId);
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
- * Depth-first traversal generator yielding nodes as visited.
1414
- *
1415
- * **O(V + E)** time, **O(V)** space.
1416
- *
1417
- * @example
1418
- * ```ts
1419
- * import { createGraph, dfs } from '@statelyai/graph';
1420
- *
1421
- * const graph = createGraph({
1422
- * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
1423
- * edges: [{ id: 'ab', sourceId: 'a', targetId: 'b' }, { id: 'bc', sourceId: 'b', targetId: 'c' }],
1424
- * });
1425
- *
1426
- * for (const node of dfs(graph, 'a')) {
1427
- * console.log(node.id); // 'a', 'b', 'c'
1428
- * }
1429
- * ```
1430
- */
1431
- function* dfs(graph, startId) {
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 visited = /* @__PURE__ */ new Set();
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 id = stack.pop();
1437
- if (visited.has(id)) continue;
1438
- visited.add(id);
1439
- const ni = idx.nodeById.get(id);
1440
- if (ni === void 0) continue;
1441
- yield graph.nodes[ni];
1442
- for (const nId of getNeighborIds(graph, id)) if (!visited.has(nId)) stack.push(nId);
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 getNeighborIds(graph, nodeId) {
1464
+ function getPostorder(graph, opts) {
1446
1465
  const idx = getIndex(graph);
1447
- const ids = [];
1448
- for (const eid of idx.outEdges.get(nodeId) ?? []) {
1449
- const ai = idx.edgeById.get(eid);
1450
- if (ai !== void 0) ids.push(graph.edges[ai].targetId);
1451
- }
1452
- if (graph.type === "undirected") for (const eid of idx.inEdges.get(nodeId) ?? []) {
1453
- const ai = idx.edgeById.get(eid);
1454
- if (ai !== void 0) ids.push(graph.edges[ai].sourceId);
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 ids;
1483
+ return result;
1457
1484
  }
1458
- function getSuccessorIds(graph, nodeId) {
1459
- const idx = getIndex(graph);
1460
- return (idx.outEdges.get(nodeId) ?? []).map((eid) => graph.edges[idx.edgeById.get(eid)].targetId);
1485
+ function getPreorders(graph, opts) {
1486
+ return [...genPreorders(graph, opts)];
1461
1487
  }
1462
- var MinPriorityQueue = class {
1463
- items = [];
1464
- constructor(compare) {
1465
- this.compare = compare;
1466
- }
1467
- get size() {
1468
- return this.items.length;
1469
- }
1470
- push(item) {
1471
- this.items.push(item);
1472
- this.bubbleUp(this.items.length - 1);
1473
- }
1474
- pop() {
1475
- if (this.items.length === 0) return void 0;
1476
- const first = this.items[0];
1477
- const last = this.items.pop();
1478
- if (this.items.length > 0) {
1479
- this.items[0] = last;
1480
- this.bubbleDown(0);
1481
- }
1482
- return first;
1483
- }
1484
- bubbleUp(index) {
1485
- let current = index;
1486
- while (current > 0) {
1487
- const parent = Math.floor((current - 1) / 2);
1488
- if (this.compare(this.items[current], this.items[parent]) >= 0) break;
1489
- [this.items[current], this.items[parent]] = [this.items[parent], this.items[current]];
1490
- current = parent;
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
- bubbleDown(index) {
1494
- let current = index;
1495
- while (true) {
1496
- const left = current * 2 + 1;
1497
- const right = left + 1;
1498
- let smallest = current;
1499
- if (left < this.items.length && this.compare(this.items[left], this.items[smallest]) < 0) smallest = left;
1500
- if (right < this.items.length && this.compare(this.items[right], this.items[smallest]) < 0) smallest = right;
1501
- if (smallest === current) break;
1502
- [this.items[current], this.items[smallest]] = [this.items[smallest], this.items[current]];
1503
- current = smallest;
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
- function isAcyclicUndirected(graph) {
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 visited = /* @__PURE__ */ new Set();
1545
- const hasCycle = (id, parentId) => {
1546
- visited.add(id);
1547
- for (const eid of idx.outEdges.get(id) ?? []) {
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 neighborId = graph.edges[ai].targetId;
1551
- if (!visited.has(neighborId)) {
1552
- if (hasCycle(neighborId, id)) return true;
1553
- } else if (neighborId !== parentId) return true;
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(id) ?? []) {
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 neighborId = graph.edges[ai].sourceId;
1559
- if (!visited.has(neighborId)) {
1560
- if (hasCycle(neighborId, id)) return true;
1561
- } else if (neighborId !== parentId) return true;
1617
+ const edge = graph.edges[ai];
1618
+ if (!inMST.has(edge.sourceId)) candidates.push({
1619
+ weight: getWeight(edge),
1620
+ edge
1621
+ });
1562
1622
  }
1563
- return false;
1564
- };
1565
- for (const n of graph.nodes) if (!visited.has(n.id) && hasCycle(n.id, null)) return false;
1566
- return true;
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 connected components as arrays of nodes.
1570
- * Treats all edges as undirected for connectivity.
1727
+ * Returns degree centrality scores for all nodes.
1571
1728
  *
1572
- * **O(V + E)** time.
1729
+ * Degree centrality is the node degree normalized by `n - 1`.
1573
1730
  *
1574
1731
  * @example
1575
1732
  * ```ts
1576
- * import { createGraph, getConnectedComponents } from '@statelyai/graph';
1577
- *
1578
- * const graph = createGraph({
1579
- * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
1580
- * edges: [{ id: 'ab', sourceId: 'a', targetId: 'b' }],
1581
- * });
1582
- *
1583
- * const components = getConnectedComponents(graph);
1584
- * // [[nodeA, nodeB], [nodeC]]
1733
+ * const scores = getDegreeCentrality(graph);
1734
+ * console.log(scores.a); // 0.5
1585
1735
  * ```
1586
1736
  */
1587
- function getConnectedComponents(graph) {
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 visited = /* @__PURE__ */ new Set();
1590
- const components = [];
1591
- for (const n of graph.nodes) {
1592
- if (visited.has(n.id)) continue;
1593
- const component = [];
1594
- const queue = [n.id];
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 components;
1747
+ return scores;
1622
1748
  }
1623
1749
  /**
1624
- * Returns a topological ordering of nodes, or `null` if the graph is cyclic.
1625
- *
1626
- * **O(V + E)** time (Kahn's algorithm).
1627
- *
1628
- * @example
1629
- * ```ts
1630
- * import { createGraph, getTopologicalSort } from '@statelyai/graph';
1631
- *
1632
- * const graph = createGraph({
1633
- * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
1634
- * edges: [
1635
- * { id: 'ab', sourceId: 'a', targetId: 'b' },
1636
- * { id: 'bc', sourceId: 'b', targetId: 'c' },
1637
- * ],
1638
- * });
1750
+ * Returns in-degree centrality scores for all nodes.
1639
1751
  *
1640
- * const sorted = getTopologicalSort(graph);
1641
- * // [nodeA, nodeB, nodeC]
1642
- * ```
1752
+ * In-degree centrality is the incoming degree normalized by `n - 1`.
1643
1753
  */
1644
- function getTopologicalSort(graph) {
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 inDeg = /* @__PURE__ */ new Map();
1647
- for (const n of graph.nodes) inDeg.set(n.id, 0);
1648
- for (const e of graph.edges) inDeg.set(e.targetId, (inDeg.get(e.targetId) ?? 0) + 1);
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
- * Checks whether a path exists between two nodes.
1670
- *
1671
- * **O(V + E)** time (BFS) or **O((V + E) log V)** (Dijkstra when weighted).
1672
- *
1673
- * @example
1674
- * ```ts
1675
- * import { createGraph, hasPath } from '@statelyai/graph';
1676
- *
1677
- * const graph = createGraph({
1678
- * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
1679
- * edges: [{ id: 'ab', sourceId: 'a', targetId: 'b' }],
1680
- * });
1762
+ * Returns out-degree centrality scores for all nodes.
1681
1763
  *
1682
- * hasPath(graph, 'a', 'b'); // true
1683
- * hasPath(graph, 'a', 'c'); // false
1684
- * ```
1764
+ * Out-degree centrality is the outgoing degree normalized by `n - 1`.
1685
1765
  */
1686
- function hasPath(graph, sourceId, targetId) {
1687
- return getShortestPaths(graph, {
1688
- from: sourceId,
1689
- to: targetId
1690
- }).length > 0;
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
- * Checks whether the graph is connected (all nodes reachable from any node).
1694
- *
1695
- * **O(V + E)** time.
1696
- *
1697
- * @example
1698
- * ```ts
1699
- * import { createGraph, isConnected } from '@statelyai/graph';
1700
- *
1701
- * const graph = createGraph({
1702
- * nodes: [{ id: 'a' }, { id: 'b' }],
1703
- * edges: [{ id: 'ab', sourceId: 'a', targetId: 'b' }],
1704
- * });
1774
+ * Returns closeness centrality scores for all nodes.
1705
1775
  *
1706
- * isConnected(graph); // true
1707
- * ```
1776
+ * Distances are computed over unweighted shortest paths using the graph's
1777
+ * existing directed or undirected edge semantics.
1708
1778
  */
1709
- function isConnected(graph) {
1710
- if (graph.nodes.length === 0) return true;
1711
- return getConnectedComponents(graph).length <= 1;
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
- * Checks whether the graph is a tree (connected and acyclic).
1715
- *
1716
- * **O(V + E)** time.
1717
- *
1718
- * @example
1719
- * ```ts
1720
- * import { createGraph, isTree } from '@statelyai/graph';
1721
- *
1722
- * const tree = createGraph({
1723
- * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
1724
- * edges: [
1725
- * { id: 'ab', sourceId: 'a', targetId: 'b' },
1726
- * { id: 'ac', sourceId: 'a', targetId: 'c' },
1727
- * ],
1728
- * });
1795
+ * Returns betweenness centrality scores for all nodes.
1729
1796
  *
1730
- * isTree(tree); // true
1731
- * ```
1797
+ * Uses Brandes' algorithm over unweighted shortest paths and returns
1798
+ * normalized scores.
1732
1799
  */
1733
- function isTree(graph) {
1734
- return isConnected(graph) && isAcyclic(graph);
1735
- }
1736
- /** Resolve the `from` node ID from opts or graph defaults. */
1737
- function resolveFrom(graph, opts) {
1738
- if (opts?.from) return opts.from;
1739
- if (graph.initialNodeId) return graph.initialNodeId;
1740
- const inDeg = /* @__PURE__ */ new Map();
1741
- for (const n of graph.nodes) inDeg.set(n.id, 0);
1742
- for (const e of graph.edges) inDeg.set(e.targetId, (inDeg.get(e.targetId) ?? 0) + 1);
1743
- const roots = [...inDeg.entries()].filter(([, d]) => d === 0).map(([id]) => id);
1744
- if (roots.length === 1) return roots[0];
1745
- throw new Error("Cannot determine start node — provide opts.from or set graph.initialNodeId");
1746
- }
1747
- /** Get neighbor IDs with their connecting edges. */
1748
- function getNeighborEdges(graph, nodeId) {
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
- return result;
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 id = queue.shift();
1789
- const d = dist.get(id);
1790
- for (const { neighborId, edge } of getNeighborEdges(graph, id)) {
1791
- const newDist = d + 1;
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
- } else if (existing === newDist) prev.get(neighborId).push({
1801
- from: id,
1802
- edge
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
- } else {
1807
- const effectiveWeight = getWeight ?? ((e) => e.weight ?? 1);
1808
- const visited = /* @__PURE__ */ new Set();
1809
- const pq = new MinPriorityQueue((a, b) => a.dist - b.dist);
1810
- pq.push({
1811
- id: sourceId,
1812
- dist: 0
1813
- });
1814
- while (pq.size > 0) {
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
- return {
1839
- dist,
1840
- prev
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
- * Bellman-Ford single-source shortest paths.
1845
- * **O(VE)** time. Handles negative edge weights.
1846
- * Throws if a negative-weight cycle is reachable from the source.
1852
+ * Returns PageRank scores for all nodes.
1853
+ *
1854
+ * Uses power iteration with damping factor `alpha`.
1847
1855
  */
1848
- function bellmanFord(graph, sourceId, getWeight) {
1849
- const dist = /* @__PURE__ */ new Map();
1850
- const prev = /* @__PURE__ */ new Map();
1851
- const effectiveWeight = getWeight ?? ((e) => e.weight ?? 1);
1852
- const isUndirected = graph.type === "undirected";
1853
- for (const node of graph.nodes) {
1854
- dist.set(node.id, Infinity);
1855
- prev.set(node.id, []);
1856
- }
1857
- dist.set(sourceId, 0);
1858
- const V = graph.nodes.length;
1859
- const directedEdges = [];
1860
- for (const edge of graph.edges) {
1861
- directedEdges.push({
1862
- fromId: edge.sourceId,
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 (!changed) break;
1895
- }
1896
- for (const { fromId, toId, edge } of directedEdges) {
1897
- const d = dist.get(fromId);
1898
- if (d === Infinity) continue;
1899
- if (d + effectiveWeight(edge) < dist.get(toId)) throw new Error("Graph contains a negative-weight cycle reachable from the source node");
1900
- }
1901
- for (const [id, d] of dist) if (d === Infinity) {
1902
- dist.delete(id);
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 preds = prev.get(targetId);
1920
- if (!preds || preds.length === 0) return;
1921
- const targetNi = getIndex(graph).nodeById.get(targetId);
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 all shortest paths from a source node as an array.
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
- * const paths = getShortestPaths(graph);
1985
- * // paths to 'b' and 'c' from 'a'
1986
- * ```
1892
+ * Uses power iteration and L2 normalization per iteration.
1987
1893
  */
1988
- function getShortestPaths(graph, opts) {
1989
- return [...genShortestPaths(graph, opts)];
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 a single shortest path from source to target, or `undefined` if unreachable.
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
- * const path = getShortestPath(graph, { to: 'c' });
2010
- * // path.steps -> [{node: nodeB, edge: ...}, {node: nodeC, edge: ...}]
2011
- * ```
1925
+ * Uses power iteration over incoming neighbors for directed graphs and
1926
+ * undirected adjacency for undirected graphs.
2012
1927
  */
2013
- function getShortestPath(graph, opts) {
2014
- for (const path of genShortestPaths(graph, opts)) return path;
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
- * Returns all simple (acyclic) paths from a source node as an array.
2018
- * Delegates to `genSimplePaths` internally.
2019
- *
2020
- * **O(V!)** worst-case (exponential in dense graphs).
2021
- *
2022
- * @example
2023
- * ```ts
2024
- * import { createGraph, getSimplePaths } from '@statelyai/graph';
2025
- *
2026
- * const graph = createGraph({
2027
- * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
2028
- * edges: [
2029
- * { id: 'ab', sourceId: 'a', targetId: 'b' },
2030
- * { id: 'bc', sourceId: 'b', targetId: 'c' },
2031
- * { id: 'ac', sourceId: 'a', targetId: 'c' },
2032
- * ],
2033
- * initialNodeId: 'a',
2034
- * });
2035
- *
2036
- * const paths = getSimplePaths(graph, { to: 'c' });
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 currentSteps = [];
2077
- function* dfsCollect(nodeId) {
2078
- visited.add(nodeId);
2079
- if (targetId !== void 0) {
2080
- if (nodeId === targetId) {
2081
- yield {
2082
- source: sourceNode,
2083
- steps: [...currentSteps]
2084
- };
2085
- visited.delete(nodeId);
2086
- return;
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
- visited.delete(nodeId);
1986
+ communities.push(community.sort((a, b) => a.id.localeCompare(b.id)));
2103
1987
  }
2104
- yield* dfsCollect(sourceId);
1988
+ return communities.sort((a, b) => a[0].id.localeCompare(b[0].id));
2105
1989
  }
2106
- /**
2107
- * Returns a single simple (acyclic) path from source to target, or `undefined` if unreachable.
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
- * Returns strongly connected components using Tarjan's algorithm.
2133
- * Only meaningful for directed graphs.
2134
- *
2135
- * **O(V + E)** time.
2136
- *
2137
- * @example
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
- for (const n of graph.nodes) if (!nodeIndex.has(n.id)) strongconnect(n.id);
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* genCyclesDirected(graph) {
2245
- const idx = getIndex(graph);
2246
- const sortedIds = graph.nodes.map((n) => n.id).sort();
2247
- for (let si = 0; si < sortedIds.length; si++) {
2248
- const startId = sortedIds[si];
2249
- const allowed = new Set(sortedIds.slice(si));
2250
- const visited = /* @__PURE__ */ new Set();
2251
- const steps = [];
2252
- const startNi = idx.nodeById.get(startId);
2253
- const startNode = graph.nodes[startNi];
2254
- const found = [];
2255
- function dfsFind(currentId) {
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
- dfsFind(startId);
2282
- yield* found;
2283
- }
2284
- }
2285
- function* genCyclesUndirected(graph) {
2286
- const idx = getIndex(graph);
2287
- const sortedIds = graph.nodes.map((n) => n.id).sort();
2288
- const seen = /* @__PURE__ */ new Set();
2289
- for (let si = 0; si < sortedIds.length; si++) {
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 (neighborId === startId && steps.length >= 2) {
2305
- const innerIds = steps.map((s) => s.node.id).sort().join(",");
2306
- if (!seen.has(innerIds)) {
2307
- seen.add(innerIds);
2308
- found.push({
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
- visited.add(next);
2443
- stack.push(next);
2444
- }
2445
- return result;
2446
- }
2447
- /**
2448
- * Returns all possible preorder sequences as an array. Can be exponential -- prefer `genPreorders`.
2449
- *
2450
- * **O(V! · V)** worst-case (exponential).
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
- * Lazily yields all possible postorder (DFS finish-order) sequences.
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
- * for (const order of genPostorders(graph)) {
2584
- * console.log(order.map(n => n.id));
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* genPostorders(graph, opts) {
2590
- const idx = getIndex(graph);
2591
- const startId = resolveFrom(graph, opts);
2592
- if (idx.nodeById.get(startId) === void 0) return;
2593
- const queue = [{
2594
- visited: new Set([startId]),
2595
- postorder: [],
2596
- dfsStack: [startId]
2597
- }];
2598
- while (queue.length > 0) {
2599
- const frame = queue.pop();
2600
- const { visited, dfsStack } = frame;
2601
- let { postorder } = frame;
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
- for (const nextId of unvisited) {
2613
- if (idx.nodeById.get(nextId) === void 0) continue;
2614
- const newVisited = new Set(visited);
2615
- newVisited.add(nextId);
2616
- queue.push({
2617
- visited: newVisited,
2618
- postorder: [...postorder],
2619
- dfsStack: [...dfsStack, nextId]
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
- if (!branched) yield postorder;
2091
+ labels = nextLabels;
2092
+ if (!changed) break;
2626
2093
  }
2094
+ return normalizeCommunities(graph, labels);
2627
2095
  }
2628
2096
  /**
2629
- * Returns a minimum spanning tree of the graph.
2630
- * Only meaningful for connected undirected graphs (or the component reachable
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 getMinimumSpanningTree(graph, opts) {
2656
- const algorithm = opts?.algorithm ?? "prim";
2657
- const getWeight = opts?.getWeight ?? ((e) => e.weight ?? 1);
2658
- const mstEdges = algorithm === "kruskal" ? kruskalMST(graph, getWeight) : primMST(graph, getWeight);
2659
- return createGraph({
2660
- id: graph.id,
2661
- type: graph.type,
2662
- initialNodeId: graph.initialNodeId ?? void 0,
2663
- nodes: graph.nodes.map((n) => ({
2664
- id: n.id,
2665
- parentId: n.parentId ?? void 0,
2666
- initialNodeId: n.initialNodeId ?? void 0,
2667
- label: n.label,
2668
- data: n.data
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
- const startId = graph.nodes[0].id;
2707
- inMST.add(startId);
2708
- addEdgesOf(startId);
2709
- while (candidates.size > 0 && inMST.size < graph.nodes.length) {
2710
- const { edge } = candidates.pop();
2711
- const targetId = graph.type === "undirected" && inMST.has(edge.targetId) ? edge.sourceId : edge.targetId;
2712
- if (inMST.has(targetId)) continue;
2713
- inMST.add(targetId);
2714
- mstEdges.push(edge);
2715
- addEdgesOf(targetId);
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 mstEdges;
2134
+ return last;
2718
2135
  }
2719
- function kruskalMST(graph, getWeight) {
2720
- const sorted = [...graph.edges].sort((a, b) => getWeight(a) - getWeight(b));
2721
- const parent = /* @__PURE__ */ new Map();
2722
- const rank = /* @__PURE__ */ new Map();
2723
- for (const n of graph.nodes) {
2724
- parent.set(n.id, n.id);
2725
- rank.set(n.id, 0);
2726
- }
2727
- function find(x) {
2728
- if (parent.get(x) !== x) parent.set(x, find(parent.get(x)));
2729
- return parent.get(x);
2730
- }
2731
- function union(x, y) {
2732
- const rx = find(x), ry = find(y);
2733
- if (rx === ry) return false;
2734
- if (rank.get(rx) < rank.get(ry)) parent.set(rx, ry);
2735
- else if (rank.get(rx) > rank.get(ry)) parent.set(ry, rx);
2736
- else {
2737
- parent.set(ry, rx);
2738
- rank.set(rx, rank.get(rx) + 1);
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
- const mstEdges = [];
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 shortest paths between all pairs of nodes.
2748
- * Algorithm 'dijkstra' (default): runs getShortestPaths per source node.
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 getAllPairsShortestPaths(graph, opts) {
2771
- const algorithm = opts?.algorithm ?? "dijkstra";
2772
- if (algorithm === "floyd-warshall") return floydWarshallAllPaths(graph, opts?.getWeight);
2773
- if (algorithm === "bellman-ford") return bellmanFordAllPaths(graph, opts?.getWeight);
2774
- return dijkstraAllPaths(graph, opts?.getWeight);
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
- function bellmanFordAllPaths(graph, getWeight) {
2777
- const results = [];
2778
- for (const node of graph.nodes) {
2779
- const paths = getShortestPaths(graph, {
2780
- from: node.id,
2781
- getWeight,
2782
- algorithm: "bellman-ford"
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
- return results;
2787
- }
2788
- function dijkstraAllPaths(graph, getWeight) {
2789
- const results = [];
2790
- for (const node of graph.nodes) {
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 results;
2210
+ return neighbors;
2798
2211
  }
2799
- function floydWarshallAllPaths(graph, getWeight) {
2800
- const idx = getIndex(graph);
2801
- const w = getWeight ?? ((e) => e.weight ?? 1);
2802
- const nodeIds = graph.nodes.map((n$1) => n$1.id);
2803
- const n = nodeIds.length;
2804
- const idxOf = /* @__PURE__ */ new Map();
2805
- for (let i = 0; i < n; i++) idxOf.set(nodeIds[i], i);
2806
- const INF = Infinity;
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
- for (let k = 0; k < n; k++) for (let i = 0; i < n; i++) for (let j = 0; j < n; j++) {
2838
- if (dist[i][k] === INF || dist[k][j] === INF) continue;
2839
- const newDist = dist[i][k] + dist[k][j];
2840
- if (newDist < dist[i][j]) {
2841
- dist[i][j] = newDist;
2842
- prev[i][j] = prev[k][j].map((p) => ({ ...p }));
2843
- } else if (newDist === dist[i][j] && newDist < INF) {
2844
- for (const p of prev[k][j]) if (!prev[i][j].some((x) => x.edge.id === p.edge.id)) prev[i][j].push({ ...p });
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
- const results = [];
2848
- for (let i = 0; i < n; i++) {
2849
- const sourceNi = idx.nodeById.get(nodeIds[i]);
2850
- if (sourceNi === void 0) continue;
2851
- const sourceNode = graph.nodes[sourceNi];
2852
- for (let j = 0; j < n; j++) {
2853
- if (i === j || dist[i][j] === INF) continue;
2854
- const paths = fwReconstruct(graph, prev, idxOf, nodeIds, sourceNode, i, j);
2855
- results.push(...paths);
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
- return results;
2259
+ if (parentEdgeId === null && childCount > 1) state.articulationPoints.add(nodeId);
2859
2260
  }
2860
- function fwReconstruct(graph, prev, idxOf, nodeIds, sourceNode, sourceIdx, targetIdx) {
2861
- if (sourceIdx === targetIdx) return [{
2862
- source: sourceNode,
2863
- steps: []
2864
- }];
2865
- const preds = prev[sourceIdx][targetIdx];
2866
- if (preds.length === 0) return [];
2867
- const targetNi = getIndex(graph).nodeById.get(nodeIds[targetIdx]);
2868
- if (targetNi === void 0) return [];
2869
- const targetNode = graph.nodes[targetNi];
2870
- const results = [];
2871
- for (const { from, edge } of preds) {
2872
- const prefixPaths = fwReconstruct(graph, prev, idxOf, nodeIds, sourceNode, sourceIdx, from);
2873
- for (const prefix of prefixPaths) results.push({
2874
- source: sourceNode,
2875
- steps: [...prefix.steps, {
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 results;
2278
+ return state;
2882
2279
  }
2883
2280
  /**
2884
- * Returns a shortest path using A* search with an admissible heuristic.
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
- * @example
2891
- * ```ts
2892
- * import { createGraph, getAStarPath } from '@statelyai/graph';
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
- * const graph = createGraph({
2895
- * nodes: [
2896
- * { id: 'a', x: 0, y: 0 },
2897
- * { id: 'b', x: 1, y: 0 },
2898
- * { id: 'c', x: 1, y: 1 },
2899
- * ],
2900
- * edges: [
2901
- * { id: 'ab', sourceId: 'a', targetId: 'b', weight: 1 },
2902
- * { id: 'bc', sourceId: 'b', targetId: 'c', weight: 1 },
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
- * const path = getAStarPath(graph, {
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 getAStarPath(graph, opts) {
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 { from: sourceId, to: targetId, heuristic } = opts;
2922
- const getWeight = opts.getWeight ?? ((e) => e.weight ?? 1);
2923
- const sourceNi = idx.nodeById.get(sourceId);
2924
- if (sourceNi === void 0) return void 0;
2925
- if (!idx.nodeById.has(targetId)) return void 0;
2926
- if (sourceId === targetId) return {
2927
- source: graph.nodes[sourceNi],
2928
- steps: []
2929
- };
2930
- const gScore = /* @__PURE__ */ new Map();
2931
- const cameFrom = /* @__PURE__ */ new Map();
2932
- const closedSet = /* @__PURE__ */ new Set();
2933
- const openSet = new MinPriorityQueue((a, b) => a.f - b.f);
2934
- gScore.set(sourceId, 0);
2935
- openSet.push({
2936
- id: sourceId,
2937
- f: heuristic(sourceId)
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
- * Joins two paths end-to-end. The last node of the head path must equal
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
- * const ab = getShortestPath(graph, { to: 'b' })!;
2998
- * const bc = getShortestPath(graph, { from: 'b', to: 'c' })!;
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 joinPaths(headPath, tailPath) {
3004
- const headEnd = headPath.steps.length > 0 ? headPath.steps[headPath.steps.length - 1].node : headPath.source;
3005
- 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}"`);
3006
- return {
3007
- source: headPath.source,
3008
- steps: [...headPath.steps, ...tailPath.steps]
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 $, 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 };
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 };