@statelyai/graph 0.9.0 → 0.11.0

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