graphwise 1.0.0 → 1.1.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 (224) hide show
  1. package/README.md +41 -0
  2. package/dist/expansion/base.d.ts +12 -0
  3. package/dist/expansion/base.d.ts.map +1 -0
  4. package/dist/expansion/base.unit.test.d.ts +2 -0
  5. package/dist/expansion/base.unit.test.d.ts.map +1 -0
  6. package/dist/expansion/dome.d.ts +16 -0
  7. package/dist/expansion/dome.d.ts.map +1 -0
  8. package/dist/expansion/dome.unit.test.d.ts +2 -0
  9. package/dist/expansion/dome.unit.test.d.ts.map +1 -0
  10. package/dist/expansion/edge.d.ts +15 -0
  11. package/dist/expansion/edge.d.ts.map +1 -0
  12. package/dist/expansion/edge.unit.test.d.ts +2 -0
  13. package/dist/expansion/edge.unit.test.d.ts.map +1 -0
  14. package/dist/expansion/hae.d.ts +22 -0
  15. package/dist/expansion/hae.d.ts.map +1 -0
  16. package/dist/expansion/hae.unit.test.d.ts +2 -0
  17. package/dist/expansion/hae.unit.test.d.ts.map +1 -0
  18. package/dist/expansion/index.d.ts +22 -0
  19. package/dist/expansion/index.d.ts.map +1 -0
  20. package/dist/expansion/maze.d.ts +25 -0
  21. package/dist/expansion/maze.d.ts.map +1 -0
  22. package/dist/expansion/maze.unit.test.d.ts +2 -0
  23. package/dist/expansion/maze.unit.test.d.ts.map +1 -0
  24. package/dist/expansion/pipe.d.ts +15 -0
  25. package/dist/expansion/pipe.d.ts.map +1 -0
  26. package/dist/expansion/pipe.unit.test.d.ts +2 -0
  27. package/dist/expansion/pipe.unit.test.d.ts.map +1 -0
  28. package/dist/expansion/reach.d.ts +26 -0
  29. package/dist/expansion/reach.d.ts.map +1 -0
  30. package/dist/expansion/reach.unit.test.d.ts +2 -0
  31. package/dist/expansion/reach.unit.test.d.ts.map +1 -0
  32. package/dist/expansion/sage.d.ts +24 -0
  33. package/dist/expansion/sage.d.ts.map +1 -0
  34. package/dist/expansion/sage.unit.test.d.ts +2 -0
  35. package/dist/expansion/sage.unit.test.d.ts.map +1 -0
  36. package/dist/expansion/types.d.ts +105 -0
  37. package/dist/expansion/types.d.ts.map +1 -0
  38. package/dist/extraction/ego-network.d.ts +32 -0
  39. package/dist/extraction/ego-network.d.ts.map +1 -0
  40. package/dist/extraction/ego-network.unit.test.d.ts +5 -0
  41. package/dist/extraction/ego-network.unit.test.d.ts.map +1 -0
  42. package/dist/extraction/index.d.ts +20 -0
  43. package/dist/extraction/index.d.ts.map +1 -0
  44. package/dist/extraction/induced-subgraph.d.ts +19 -0
  45. package/dist/extraction/induced-subgraph.d.ts.map +1 -0
  46. package/dist/extraction/induced-subgraph.unit.test.d.ts +5 -0
  47. package/dist/extraction/induced-subgraph.unit.test.d.ts.map +1 -0
  48. package/dist/extraction/k-core.d.ts +24 -0
  49. package/dist/extraction/k-core.d.ts.map +1 -0
  50. package/dist/extraction/k-core.unit.test.d.ts +5 -0
  51. package/dist/extraction/k-core.unit.test.d.ts.map +1 -0
  52. package/dist/extraction/motif.d.ts +50 -0
  53. package/dist/extraction/motif.d.ts.map +1 -0
  54. package/dist/extraction/motif.unit.test.d.ts +5 -0
  55. package/dist/extraction/motif.unit.test.d.ts.map +1 -0
  56. package/dist/extraction/node-filter.d.ts +35 -0
  57. package/dist/extraction/node-filter.d.ts.map +1 -0
  58. package/dist/extraction/node-filter.unit.test.d.ts +5 -0
  59. package/dist/extraction/node-filter.unit.test.d.ts.map +1 -0
  60. package/dist/extraction/truss.d.ts +41 -0
  61. package/dist/extraction/truss.d.ts.map +1 -0
  62. package/dist/extraction/truss.unit.test.d.ts +5 -0
  63. package/dist/extraction/truss.unit.test.d.ts.map +1 -0
  64. package/dist/gpu/context.d.ts +118 -0
  65. package/dist/gpu/context.d.ts.map +1 -0
  66. package/dist/gpu/context.unit.test.d.ts +2 -0
  67. package/dist/gpu/context.unit.test.d.ts.map +1 -0
  68. package/dist/gpu/csr.d.ts +97 -0
  69. package/dist/gpu/csr.d.ts.map +1 -0
  70. package/dist/gpu/csr.unit.test.d.ts +2 -0
  71. package/dist/gpu/csr.unit.test.d.ts.map +1 -0
  72. package/dist/gpu/detect.d.ts +25 -0
  73. package/dist/gpu/detect.d.ts.map +1 -0
  74. package/dist/gpu/detect.unit.test.d.ts +2 -0
  75. package/dist/gpu/detect.unit.test.d.ts.map +1 -0
  76. package/dist/gpu/index.cjs +6 -0
  77. package/dist/gpu/index.d.ts +11 -0
  78. package/dist/gpu/index.d.ts.map +1 -0
  79. package/dist/gpu/index.js +2 -0
  80. package/dist/gpu/types.d.ts +50 -0
  81. package/dist/gpu/types.d.ts.map +1 -0
  82. package/dist/gpu-BJRVYBjx.cjs +338 -0
  83. package/dist/gpu-BJRVYBjx.cjs.map +1 -0
  84. package/dist/gpu-BveuXugy.js +315 -0
  85. package/dist/gpu-BveuXugy.js.map +1 -0
  86. package/dist/graph/adjacency-map.d.ts +95 -0
  87. package/dist/graph/adjacency-map.d.ts.map +1 -0
  88. package/dist/graph/adjacency-map.unit.test.d.ts +2 -0
  89. package/dist/graph/adjacency-map.unit.test.d.ts.map +1 -0
  90. package/dist/graph/index.cjs +3 -0
  91. package/dist/graph/index.d.ts +9 -0
  92. package/dist/graph/index.d.ts.map +1 -0
  93. package/dist/graph/index.js +2 -0
  94. package/dist/graph/interfaces.d.ts +125 -0
  95. package/dist/graph/interfaces.d.ts.map +1 -0
  96. package/dist/graph/types.d.ts +72 -0
  97. package/dist/graph/types.d.ts.map +1 -0
  98. package/dist/graph-DLWiziLB.js +222 -0
  99. package/dist/graph-DLWiziLB.js.map +1 -0
  100. package/dist/graph-az06J1YV.cjs +227 -0
  101. package/dist/graph-az06J1YV.cjs.map +1 -0
  102. package/dist/index/index.cjs +1404 -0
  103. package/dist/index/index.cjs.map +1 -0
  104. package/dist/index/index.js +1356 -0
  105. package/dist/index/index.js.map +1 -0
  106. package/dist/index.d.ts +15 -0
  107. package/dist/index.d.ts.map +1 -0
  108. package/dist/kmeans-B0HEOU6k.cjs +234 -0
  109. package/dist/kmeans-B0HEOU6k.cjs.map +1 -0
  110. package/dist/kmeans-DgbsOznU.js +223 -0
  111. package/dist/kmeans-DgbsOznU.js.map +1 -0
  112. package/dist/ranking/baselines/shortest.d.ts +15 -0
  113. package/dist/ranking/baselines/shortest.d.ts.map +1 -0
  114. package/dist/ranking/baselines/shortest.unit.test.d.ts +2 -0
  115. package/dist/ranking/baselines/shortest.unit.test.d.ts.map +1 -0
  116. package/dist/ranking/baselines/types.d.ts +30 -0
  117. package/dist/ranking/baselines/types.d.ts.map +1 -0
  118. package/dist/ranking/index.d.ts +15 -0
  119. package/dist/ranking/index.d.ts.map +1 -0
  120. package/dist/ranking/mi/adamic-adar.d.ts +13 -0
  121. package/dist/ranking/mi/adamic-adar.d.ts.map +1 -0
  122. package/dist/ranking/mi/adaptive.d.ts +16 -0
  123. package/dist/ranking/mi/adaptive.d.ts.map +1 -0
  124. package/dist/ranking/mi/etch.d.ts +7 -0
  125. package/dist/ranking/mi/etch.d.ts.map +1 -0
  126. package/dist/ranking/mi/index.d.ts +18 -0
  127. package/dist/ranking/mi/index.d.ts.map +1 -0
  128. package/dist/ranking/mi/jaccard.d.ts +13 -0
  129. package/dist/ranking/mi/jaccard.d.ts.map +1 -0
  130. package/dist/ranking/mi/mi-variants.unit.test.d.ts +2 -0
  131. package/dist/ranking/mi/mi-variants.unit.test.d.ts.map +1 -0
  132. package/dist/ranking/mi/notch.d.ts +7 -0
  133. package/dist/ranking/mi/notch.d.ts.map +1 -0
  134. package/dist/ranking/mi/scale.d.ts +7 -0
  135. package/dist/ranking/mi/scale.d.ts.map +1 -0
  136. package/dist/ranking/mi/skew.d.ts +7 -0
  137. package/dist/ranking/mi/skew.d.ts.map +1 -0
  138. package/dist/ranking/mi/span.d.ts +7 -0
  139. package/dist/ranking/mi/span.d.ts.map +1 -0
  140. package/dist/ranking/mi/types.d.ts +35 -0
  141. package/dist/ranking/mi/types.d.ts.map +1 -0
  142. package/dist/ranking/parse.d.ts +56 -0
  143. package/dist/ranking/parse.d.ts.map +1 -0
  144. package/dist/ranking/parse.unit.test.d.ts +2 -0
  145. package/dist/ranking/parse.unit.test.d.ts.map +1 -0
  146. package/dist/schemas/define.d.ts +18 -0
  147. package/dist/schemas/define.d.ts.map +1 -0
  148. package/dist/schemas/define.unit.test.d.ts +2 -0
  149. package/dist/schemas/define.unit.test.d.ts.map +1 -0
  150. package/dist/schemas/graph.d.ts +85 -0
  151. package/dist/schemas/graph.d.ts.map +1 -0
  152. package/dist/schemas/graph.unit.test.d.ts +2 -0
  153. package/dist/schemas/graph.unit.test.d.ts.map +1 -0
  154. package/dist/schemas/index.cjs +3791 -0
  155. package/dist/schemas/index.cjs.map +1 -0
  156. package/dist/schemas/index.d.ts +3 -0
  157. package/dist/schemas/index.d.ts.map +1 -0
  158. package/dist/schemas/index.js +3782 -0
  159. package/dist/schemas/index.js.map +1 -0
  160. package/dist/seeds/grasp.d.ts +79 -0
  161. package/dist/seeds/grasp.d.ts.map +1 -0
  162. package/dist/seeds/grasp.unit.test.d.ts +2 -0
  163. package/dist/seeds/grasp.unit.test.d.ts.map +1 -0
  164. package/dist/seeds/index.cjs +4 -0
  165. package/dist/seeds/index.d.ts +10 -0
  166. package/dist/seeds/index.d.ts.map +1 -0
  167. package/dist/seeds/index.js +2 -0
  168. package/dist/seeds/stratified.d.ts +85 -0
  169. package/dist/seeds/stratified.d.ts.map +1 -0
  170. package/dist/seeds/stratified.unit.test.d.ts +2 -0
  171. package/dist/seeds/stratified.unit.test.d.ts.map +1 -0
  172. package/dist/seeds-B6J9oJfU.cjs +404 -0
  173. package/dist/seeds-B6J9oJfU.cjs.map +1 -0
  174. package/dist/seeds-UNZxqm_U.js +393 -0
  175. package/dist/seeds-UNZxqm_U.js.map +1 -0
  176. package/dist/structures/index.cjs +3 -0
  177. package/dist/structures/index.d.ts +3 -0
  178. package/dist/structures/index.d.ts.map +1 -0
  179. package/dist/structures/index.js +2 -0
  180. package/dist/structures/priority-queue.d.ts +59 -0
  181. package/dist/structures/priority-queue.d.ts.map +1 -0
  182. package/dist/structures/priority-queue.unit.test.d.ts +2 -0
  183. package/dist/structures/priority-queue.unit.test.d.ts.map +1 -0
  184. package/dist/structures-BPfhfqNP.js +133 -0
  185. package/dist/structures-BPfhfqNP.js.map +1 -0
  186. package/dist/structures-CJ_S_7fs.cjs +138 -0
  187. package/dist/structures-CJ_S_7fs.cjs.map +1 -0
  188. package/dist/traversal/bfs.d.ts +50 -0
  189. package/dist/traversal/bfs.d.ts.map +1 -0
  190. package/dist/traversal/bfs.unit.test.d.ts +2 -0
  191. package/dist/traversal/bfs.unit.test.d.ts.map +1 -0
  192. package/dist/traversal/dfs.d.ts +50 -0
  193. package/dist/traversal/dfs.d.ts.map +1 -0
  194. package/dist/traversal/dfs.unit.test.d.ts +2 -0
  195. package/dist/traversal/dfs.unit.test.d.ts.map +1 -0
  196. package/dist/traversal/index.cjs +6 -0
  197. package/dist/traversal/index.d.ts +11 -0
  198. package/dist/traversal/index.d.ts.map +1 -0
  199. package/dist/traversal/index.js +2 -0
  200. package/dist/traversal-CQCjUwUJ.js +149 -0
  201. package/dist/traversal-CQCjUwUJ.js.map +1 -0
  202. package/dist/traversal-QeHaNUWn.cjs +172 -0
  203. package/dist/traversal-QeHaNUWn.cjs.map +1 -0
  204. package/dist/utils/clustering-coefficient.d.ts +38 -0
  205. package/dist/utils/clustering-coefficient.d.ts.map +1 -0
  206. package/dist/utils/clustering-coefficient.unit.test.d.ts +2 -0
  207. package/dist/utils/clustering-coefficient.unit.test.d.ts.map +1 -0
  208. package/dist/utils/entropy.d.ts +58 -0
  209. package/dist/utils/entropy.d.ts.map +1 -0
  210. package/dist/utils/entropy.unit.test.d.ts +2 -0
  211. package/dist/utils/entropy.unit.test.d.ts.map +1 -0
  212. package/dist/utils/index.cjs +13 -0
  213. package/dist/utils/index.d.ts +9 -0
  214. package/dist/utils/index.d.ts.map +1 -0
  215. package/dist/utils/index.js +3 -0
  216. package/dist/utils/kmeans.d.ts +73 -0
  217. package/dist/utils/kmeans.d.ts.map +1 -0
  218. package/dist/utils/kmeans.unit.test.d.ts +2 -0
  219. package/dist/utils/kmeans.unit.test.d.ts.map +1 -0
  220. package/dist/utils-Q_akvlMn.js +164 -0
  221. package/dist/utils-Q_akvlMn.js.map +1 -0
  222. package/dist/utils-spZa1ZvS.cjs +205 -0
  223. package/dist/utils-spZa1ZvS.cjs.map +1 -0
  224. package/package.json +136 -8
@@ -0,0 +1,1356 @@
1
+ import { t as AdjacencyMapGraph } from "../graph-DLWiziLB.js";
2
+ import { i as bfsWithPath, n as dfsWithPath, r as bfs, t as dfs } from "../traversal-CQCjUwUJ.js";
3
+ import { t as PriorityQueue } from "../structures-BPfhfqNP.js";
4
+ import { n as normaliseFeatures, t as miniBatchKMeans } from "../kmeans-DgbsOznU.js";
5
+ import { n as grasp, t as stratified } from "../seeds-UNZxqm_U.js";
6
+ import { a as approximateClusteringCoefficient, i as shannonEntropy, n as localTypeEntropy, o as batchClusteringCoefficients, r as normalisedEntropy, s as localClusteringCoefficient, t as entropyFromCounts } from "../utils-Q_akvlMn.js";
7
+ import { i as detectWebGPU, n as graphToCSR, r as GPUContext, t as csrToGPUBuffers } from "../gpu-BveuXugy.js";
8
+ //#region src/expansion/base.ts
9
+ /**
10
+ * Default priority function - degree-ordered (DOME).
11
+ */
12
+ function degreePriority(_nodeId, context) {
13
+ return context.degree;
14
+ }
15
+ /**
16
+ * Run BASE expansion algorithm.
17
+ *
18
+ * @param graph - Source graph
19
+ * @param seeds - Seed nodes for expansion
20
+ * @param config - Expansion configuration
21
+ * @returns Expansion result with discovered paths
22
+ */
23
+ function base(graph, seeds, config) {
24
+ const startTime = performance.now();
25
+ const { maxNodes = 0, maxIterations = 0, maxPaths = 0, priority = degreePriority, debug = false } = config ?? {};
26
+ if (seeds.length === 0) return emptyResult("base", startTime);
27
+ const numFrontiers = seeds.length;
28
+ const visitedByFrontier = [];
29
+ const predecessors = [];
30
+ const queues = [];
31
+ for (let i = 0; i < numFrontiers; i++) {
32
+ visitedByFrontier.push(/* @__PURE__ */ new Map());
33
+ predecessors.push(/* @__PURE__ */ new Map());
34
+ queues.push(new PriorityQueue());
35
+ const seed = seeds[i];
36
+ if (seed === void 0) continue;
37
+ const seedNode = seed.id;
38
+ predecessors[i]?.set(seedNode, null);
39
+ const seedPriority = priority(seedNode, createPriorityContext(graph, seedNode, i, visitedByFrontier, [], 0));
40
+ queues[i]?.push({
41
+ nodeId: seedNode,
42
+ frontierIndex: i,
43
+ predecessor: null
44
+ }, seedPriority);
45
+ }
46
+ const allVisited = /* @__PURE__ */ new Set();
47
+ const sampledEdges = /* @__PURE__ */ new Set();
48
+ const discoveredPaths = [];
49
+ let iterations = 0;
50
+ let edgesTraversed = 0;
51
+ let termination = "exhausted";
52
+ const continueExpansion = () => {
53
+ if (maxIterations > 0 && iterations >= maxIterations) {
54
+ termination = "limit";
55
+ return false;
56
+ }
57
+ if (maxNodes > 0 && allVisited.size >= maxNodes) {
58
+ termination = "limit";
59
+ return false;
60
+ }
61
+ if (maxPaths > 0 && discoveredPaths.length >= maxPaths) {
62
+ termination = "limit";
63
+ return false;
64
+ }
65
+ return true;
66
+ };
67
+ while (continueExpansion()) {
68
+ let lowestPriority = Number.POSITIVE_INFINITY;
69
+ let activeFrontier = -1;
70
+ for (let i = 0; i < numFrontiers; i++) {
71
+ const queue = queues[i];
72
+ if (queue !== void 0 && !queue.isEmpty()) {
73
+ const peek = queue.peek();
74
+ if (peek !== void 0 && peek.priority < lowestPriority) {
75
+ lowestPriority = peek.priority;
76
+ activeFrontier = i;
77
+ }
78
+ }
79
+ }
80
+ if (activeFrontier < 0) {
81
+ termination = "exhausted";
82
+ break;
83
+ }
84
+ const queue = queues[activeFrontier];
85
+ if (queue === void 0) break;
86
+ const entry = queue.pop();
87
+ if (entry === void 0) break;
88
+ const { nodeId, predecessor } = entry.item;
89
+ const frontierVisited = visitedByFrontier[activeFrontier];
90
+ if (frontierVisited === void 0 || frontierVisited.has(nodeId)) continue;
91
+ frontierVisited.set(nodeId, activeFrontier);
92
+ if (predecessor !== null) {
93
+ const predMap = predecessors[activeFrontier];
94
+ if (predMap !== void 0) predMap.set(nodeId, predecessor);
95
+ }
96
+ allVisited.add(nodeId);
97
+ if (debug) console.log(`[BASE] Iteration ${String(iterations)}: Frontier ${String(activeFrontier)} visiting ${nodeId}`);
98
+ for (let otherFrontier = 0; otherFrontier < numFrontiers; otherFrontier++) {
99
+ if (otherFrontier === activeFrontier) continue;
100
+ const otherVisited = visitedByFrontier[otherFrontier];
101
+ if (otherVisited === void 0) continue;
102
+ if (otherVisited.has(nodeId)) {
103
+ const path = reconstructPath(nodeId, activeFrontier, otherFrontier, predecessors, seeds);
104
+ if (path !== null) {
105
+ discoveredPaths.push(path);
106
+ if (debug) console.log(`[BASE] Path found: ${path.nodes.join(" -> ")}`);
107
+ }
108
+ }
109
+ }
110
+ const neighbours = graph.neighbours(nodeId);
111
+ for (const neighbour of neighbours) {
112
+ edgesTraversed++;
113
+ const edgeKey = nodeId < neighbour ? `${nodeId}::${neighbour}` : `${neighbour}::${nodeId}`;
114
+ sampledEdges.add(edgeKey);
115
+ const frontierVisited = visitedByFrontier[activeFrontier];
116
+ if (frontierVisited === void 0 || frontierVisited.has(neighbour)) continue;
117
+ const neighbourPriority = priority(neighbour, createPriorityContext(graph, neighbour, activeFrontier, visitedByFrontier, discoveredPaths, iterations + 1));
118
+ queue.push({
119
+ nodeId: neighbour,
120
+ frontierIndex: activeFrontier,
121
+ predecessor: nodeId
122
+ }, neighbourPriority);
123
+ }
124
+ iterations++;
125
+ }
126
+ const endTime = performance.now();
127
+ const visitedPerFrontier = visitedByFrontier.map((m) => new Set(m.keys()));
128
+ const edgeTuples = /* @__PURE__ */ new Set();
129
+ for (const edgeKey of sampledEdges) {
130
+ const parts = edgeKey.split("::");
131
+ if (parts.length === 2) {
132
+ const source = parts[0];
133
+ const target = parts[1];
134
+ if (source !== void 0 && target !== void 0) edgeTuples.add([source, target]);
135
+ }
136
+ }
137
+ return {
138
+ paths: discoveredPaths,
139
+ sampledNodes: allVisited,
140
+ sampledEdges: edgeTuples,
141
+ visitedPerFrontier,
142
+ stats: {
143
+ iterations,
144
+ nodesVisited: allVisited.size,
145
+ edgesTraversed,
146
+ pathsFound: discoveredPaths.length,
147
+ durationMs: endTime - startTime,
148
+ algorithm: "base",
149
+ termination
150
+ }
151
+ };
152
+ }
153
+ /**
154
+ * Create priority context for a node.
155
+ */
156
+ function createPriorityContext(graph, nodeId, frontierIndex, visitedByFrontier, discoveredPaths, iteration) {
157
+ const combinedVisited = /* @__PURE__ */ new Map();
158
+ for (const frontierMap of visitedByFrontier) for (const [id, idx] of frontierMap) combinedVisited.set(id, idx);
159
+ const allVisited = new Set(combinedVisited.keys());
160
+ return {
161
+ graph,
162
+ degree: graph.degree(nodeId),
163
+ frontierIndex,
164
+ visitedByFrontier: combinedVisited,
165
+ allVisited,
166
+ discoveredPaths,
167
+ iteration
168
+ };
169
+ }
170
+ /**
171
+ * Reconstruct path from collision point.
172
+ */
173
+ function reconstructPath(collisionNode, frontierA, frontierB, predecessors, seeds) {
174
+ const pathA = [collisionNode];
175
+ const predA = predecessors[frontierA];
176
+ if (predA !== void 0) {
177
+ let node = collisionNode;
178
+ let next = predA.get(node);
179
+ while (next !== null && next !== void 0) {
180
+ node = next;
181
+ pathA.unshift(node);
182
+ next = predA.get(node);
183
+ }
184
+ }
185
+ const pathB = [];
186
+ const predB = predecessors[frontierB];
187
+ if (predB !== void 0) {
188
+ let node = collisionNode;
189
+ let next = predB.get(node);
190
+ while (next !== null && next !== void 0) {
191
+ node = next;
192
+ pathB.push(node);
193
+ next = predB.get(node);
194
+ }
195
+ }
196
+ const fullPath = [...pathA, ...pathB];
197
+ const seedA = seeds[frontierA];
198
+ const seedB = seeds[frontierB];
199
+ if (seedA === void 0 || seedB === void 0) return null;
200
+ return {
201
+ fromSeed: seedA,
202
+ toSeed: seedB,
203
+ nodes: fullPath
204
+ };
205
+ }
206
+ /**
207
+ * Create an empty result for early termination.
208
+ */
209
+ function emptyResult(algorithm, startTime) {
210
+ return {
211
+ paths: [],
212
+ sampledNodes: /* @__PURE__ */ new Set(),
213
+ sampledEdges: /* @__PURE__ */ new Set(),
214
+ visitedPerFrontier: [],
215
+ stats: {
216
+ iterations: 0,
217
+ nodesVisited: 0,
218
+ edgesTraversed: 0,
219
+ pathsFound: 0,
220
+ durationMs: performance.now() - startTime,
221
+ algorithm,
222
+ termination: "exhausted"
223
+ }
224
+ };
225
+ }
226
+ //#endregion
227
+ //#region src/expansion/dome.ts
228
+ /**
229
+ * Run DOME expansion (degree-ordered).
230
+ *
231
+ * @param graph - Source graph
232
+ * @param seeds - Seed nodes for expansion
233
+ * @param config - Expansion configuration
234
+ * @returns Expansion result with discovered paths
235
+ */
236
+ function dome(graph, seeds, config) {
237
+ const domePriority = (nodeId, context) => {
238
+ return graph.degree(nodeId);
239
+ };
240
+ return base(graph, seeds, {
241
+ ...config,
242
+ priority: domePriority
243
+ });
244
+ }
245
+ //#endregion
246
+ //#region src/expansion/edge.ts
247
+ /**
248
+ * EDGE priority function.
249
+ *
250
+ * Priority = degree(source) + degree(target)
251
+ * Lower values = higher priority (explored first)
252
+ */
253
+ function edgePriority(nodeId, context) {
254
+ const graph = context.graph;
255
+ let totalDegree = context.degree;
256
+ for (const neighbour of graph.neighbours(nodeId)) totalDegree += graph.degree(neighbour);
257
+ return totalDegree;
258
+ }
259
+ /**
260
+ * Run EDGE expansion algorithm.
261
+ *
262
+ * Expands from seeds prioritising low-degree edges first.
263
+ * Useful for avoiding hubs and exploring sparse regions.
264
+ *
265
+ * @param graph - Source graph
266
+ * @param seeds - Seed nodes for expansion
267
+ * @param config - Expansion configuration
268
+ * @returns Expansion result with discovered paths
269
+ */
270
+ function edge(graph, seeds, config) {
271
+ return base(graph, seeds, {
272
+ ...config,
273
+ priority: edgePriority
274
+ });
275
+ }
276
+ //#endregion
277
+ //#region src/ranking/mi/jaccard.ts
278
+ /**
279
+ * Compute Jaccard similarity between neighbourhoods of two nodes.
280
+ *
281
+ * @param graph - Source graph
282
+ * @param source - Source node ID
283
+ * @param target - Target node ID
284
+ * @param config - Optional configuration
285
+ * @returns Jaccard coefficient in [0, 1]
286
+ */
287
+ function jaccard(graph, source, target, config) {
288
+ const { epsilon = 1e-10 } = config ?? {};
289
+ const sourceNeighbours = new Set(graph.neighbours(source));
290
+ const targetNeighbours = new Set(graph.neighbours(target));
291
+ sourceNeighbours.delete(target);
292
+ targetNeighbours.delete(source);
293
+ let intersectionSize = 0;
294
+ for (const neighbour of sourceNeighbours) if (targetNeighbours.has(neighbour)) intersectionSize++;
295
+ const unionSize = sourceNeighbours.size + targetNeighbours.size - intersectionSize;
296
+ if (unionSize === 0) return 0;
297
+ const score = intersectionSize / unionSize;
298
+ return Math.max(epsilon, score);
299
+ }
300
+ //#endregion
301
+ //#region src/expansion/hae.ts
302
+ /**
303
+ * HAE priority function.
304
+ *
305
+ * Priority = 1 - MI(source, neighbour)
306
+ * Higher MI = lower priority value = explored first
307
+ */
308
+ function haePriority(nodeId, context, mi) {
309
+ const graph = context.graph;
310
+ const frontierIndex = context.frontierIndex;
311
+ let maxMi = 0;
312
+ let totalMi = 0;
313
+ let count = 0;
314
+ for (const [visitedId, idx] of context.visitedByFrontier) if (idx === frontierIndex && visitedId !== nodeId) {
315
+ const edgeMi = mi(graph, visitedId, nodeId);
316
+ totalMi += edgeMi;
317
+ count++;
318
+ if (edgeMi > maxMi) maxMi = edgeMi;
319
+ }
320
+ return 1 - (count > 0 ? totalMi / count : 0);
321
+ }
322
+ /**
323
+ * Run HAE expansion algorithm.
324
+ *
325
+ * Expands from seeds prioritising high-MI edges.
326
+ * Useful for finding paths with strong semantic associations.
327
+ *
328
+ * @param graph - Source graph
329
+ * @param seeds - Seed nodes for expansion
330
+ * @param config - Expansion configuration with MI function
331
+ * @returns Expansion result with discovered paths
332
+ */
333
+ function hae(graph, seeds, config) {
334
+ const { mi = jaccard, ...restConfig } = config ?? {};
335
+ const priority = (nodeId, context) => haePriority(nodeId, context, mi);
336
+ return base(graph, seeds, {
337
+ ...restConfig,
338
+ priority
339
+ });
340
+ }
341
+ //#endregion
342
+ //#region src/expansion/pipe.ts
343
+ /**
344
+ * PIPE priority function.
345
+ *
346
+ * Priority = 1 / (1 + bridge_score)
347
+ * Bridge score = neighbourhood overlap with other frontiers
348
+ * Higher bridge score = more likely to be on paths = explored first
349
+ */
350
+ function pipePriority(nodeId, context) {
351
+ const graph = context.graph;
352
+ const currentFrontier = context.frontierIndex;
353
+ const nodeNeighbours = new Set(graph.neighbours(nodeId));
354
+ let bridgeScore = 0;
355
+ for (const [visitedId, frontierIdx] of context.visitedByFrontier) if (frontierIdx !== currentFrontier && nodeNeighbours.has(visitedId)) bridgeScore++;
356
+ for (const path of context.discoveredPaths) if (path.nodes.includes(nodeId)) bridgeScore += 2;
357
+ return 1 / (1 + bridgeScore);
358
+ }
359
+ /**
360
+ * Run PIPE expansion algorithm.
361
+ *
362
+ * Expands from seeds prioritising bridge nodes.
363
+ * Useful for finding paths through structurally important nodes.
364
+ *
365
+ * @param graph - Source graph
366
+ * @param seeds - Seed nodes for expansion
367
+ * @param config - Expansion configuration
368
+ * @returns Expansion result with discovered paths
369
+ */
370
+ function pipe(graph, seeds, config) {
371
+ return base(graph, seeds, {
372
+ ...config,
373
+ priority: pipePriority
374
+ });
375
+ }
376
+ //#endregion
377
+ //#region src/expansion/sage.ts
378
+ /**
379
+ * SAGE priority function.
380
+ *
381
+ * Combines degree with salience:
382
+ * Priority = (1 - w) * degree + w * (1 - avg_salience)
383
+ * Lower values = higher priority
384
+ */
385
+ function sagePriority(nodeId, context, mi, salienceWeight) {
386
+ const graph = context.graph;
387
+ const degree = context.degree;
388
+ const frontierIndex = context.frontierIndex;
389
+ let totalSalience = 0;
390
+ let count = 0;
391
+ for (const [visitedId, idx] of context.visitedByFrontier) if (idx === frontierIndex && visitedId !== nodeId) {
392
+ totalSalience += mi(graph, visitedId, nodeId);
393
+ count++;
394
+ }
395
+ const avgSalience = count > 0 ? totalSalience / count : 0;
396
+ return (1 - salienceWeight) * degree + salienceWeight * (1 - avgSalience);
397
+ }
398
+ /**
399
+ * Run SAGE expansion algorithm.
400
+ *
401
+ * Combines structural exploration with semantic salience.
402
+ * Useful for finding paths that are both short and semantically meaningful.
403
+ *
404
+ * @param graph - Source graph
405
+ * @param seeds - Seed nodes for expansion
406
+ * @param config - Expansion configuration with MI function
407
+ * @returns Expansion result with discovered paths
408
+ */
409
+ function sage(graph, seeds, config) {
410
+ const { mi = jaccard, salienceWeight = .5, ...restConfig } = config ?? {};
411
+ const priority = (nodeId, context) => sagePriority(nodeId, context, mi, salienceWeight);
412
+ return base(graph, seeds, {
413
+ ...restConfig,
414
+ priority
415
+ });
416
+ }
417
+ //#endregion
418
+ //#region src/expansion/reach.ts
419
+ /**
420
+ * REACH priority function (phase 2).
421
+ *
422
+ * Uses learned MI threshold to prioritise high-MI edges.
423
+ */
424
+ function reachPriority(nodeId, context, mi, miThreshold) {
425
+ const graph = context.graph;
426
+ const frontierIndex = context.frontierIndex;
427
+ let totalMi = 0;
428
+ let count = 0;
429
+ for (const [visitedId, idx] of context.visitedByFrontier) if (idx === frontierIndex && visitedId !== nodeId) {
430
+ totalMi += mi(graph, visitedId, nodeId);
431
+ count++;
432
+ }
433
+ const avgMi = count > 0 ? totalMi / count : 0;
434
+ if (avgMi >= miThreshold) return 1 - avgMi;
435
+ else return context.degree + 100;
436
+ }
437
+ /**
438
+ * Run REACH expansion algorithm.
439
+ *
440
+ * Two-phase adaptive expansion that learns MI thresholds
441
+ * from initial sampling, then uses them for guided expansion.
442
+ *
443
+ * @param graph - Source graph
444
+ * @param seeds - Seed nodes for expansion
445
+ * @param config - Expansion configuration
446
+ * @returns Expansion result with discovered paths
447
+ */
448
+ function reach(graph, seeds, config) {
449
+ const { mi = jaccard, miThreshold = .25, ...restConfig } = config ?? {};
450
+ const priority = (nodeId, context) => reachPriority(nodeId, context, mi, miThreshold);
451
+ return base(graph, seeds, {
452
+ ...restConfig,
453
+ priority
454
+ });
455
+ }
456
+ //#endregion
457
+ //#region src/expansion/maze.ts
458
+ /**
459
+ * Compute local density around a node.
460
+ */
461
+ function localDensity(graph, nodeId) {
462
+ const neighbours = Array.from(graph.neighbours(nodeId));
463
+ const degree = neighbours.length;
464
+ if (degree < 2) return 0;
465
+ let edges = 0;
466
+ for (let i = 0; i < neighbours.length; i++) for (let j = i + 1; j < neighbours.length; j++) {
467
+ const ni = neighbours[i];
468
+ const nj = neighbours[j];
469
+ if (ni !== void 0 && nj !== void 0 && graph.getEdge(ni, nj) !== void 0) edges++;
470
+ }
471
+ const maxEdges = degree * (degree - 1) / 2;
472
+ return edges / maxEdges;
473
+ }
474
+ /**
475
+ * Compute bridge score (how many other frontiers visit neighbours).
476
+ */
477
+ function bridgeScore(nodeId, context) {
478
+ const currentFrontier = context.frontierIndex;
479
+ const nodeNeighbours = new Set(context.graph.neighbours(nodeId));
480
+ let score = 0;
481
+ for (const [visitedId, idx] of context.visitedByFrontier) if (idx !== currentFrontier && nodeNeighbours.has(visitedId)) score++;
482
+ return score;
483
+ }
484
+ /**
485
+ * MAZE adaptive priority function.
486
+ *
487
+ * Switches strategies based on local conditions:
488
+ * - High density + low bridge: EDGE mode
489
+ * - Low density + low bridge: DOME mode
490
+ * - High bridge score: PIPE mode
491
+ */
492
+ function mazePriority(nodeId, context, densityThreshold, bridgeThreshold) {
493
+ const graph = context.graph;
494
+ const degree = context.degree;
495
+ const density = localDensity(graph, nodeId);
496
+ const bridge = bridgeScore(nodeId, context);
497
+ const numFrontiers = new Set(context.visitedByFrontier.values()).size;
498
+ if ((numFrontiers > 0 ? bridge / numFrontiers : 0) >= bridgeThreshold) return 1 / (1 + bridge);
499
+ else if (density >= densityThreshold) return -degree;
500
+ else return degree;
501
+ }
502
+ /**
503
+ * Run MAZE expansion algorithm.
504
+ *
505
+ * Adaptively switches between expansion strategies based on
506
+ * local graph structure. Useful for heterogeneous graphs
507
+ * with varying density.
508
+ *
509
+ * @param graph - Source graph
510
+ * @param seeds - Seed nodes for expansion
511
+ * @param config - Expansion configuration
512
+ * @returns Expansion result with discovered paths
513
+ */
514
+ function maze(graph, seeds, config) {
515
+ const { densityThreshold = .5, bridgeThreshold = .3, ...restConfig } = config ?? {};
516
+ const priority = (nodeId, context) => mazePriority(nodeId, context, densityThreshold, bridgeThreshold);
517
+ return base(graph, seeds, {
518
+ ...restConfig,
519
+ priority
520
+ });
521
+ }
522
+ //#endregion
523
+ //#region src/ranking/parse.ts
524
+ /**
525
+ * Rank paths using PARSE (Path-Aware Ranking via Salience Estimation).
526
+ *
527
+ * Computes geometric mean of edge MI scores for each path,
528
+ * then sorts by salience (highest first).
529
+ *
530
+ * @param graph - Source graph
531
+ * @param paths - Paths to rank
532
+ * @param config - Configuration options
533
+ * @returns Ranked paths with statistics
534
+ */
535
+ function parse(graph, paths, config) {
536
+ const startTime = performance.now();
537
+ const { mi = jaccard, epsilon = 1e-10 } = config ?? {};
538
+ const rankedPaths = [];
539
+ for (const path of paths) {
540
+ const salience = computePathSalience(graph, path, mi, epsilon);
541
+ rankedPaths.push({
542
+ ...path,
543
+ salience
544
+ });
545
+ }
546
+ rankedPaths.sort((a, b) => b.salience - a.salience);
547
+ const endTime = performance.now();
548
+ const saliences = rankedPaths.map((p) => p.salience);
549
+ const meanSalience = saliences.length > 0 ? saliences.reduce((a, b) => a + b, 0) / saliences.length : 0;
550
+ const sortedSaliences = [...saliences].sort((a, b) => a - b);
551
+ const mid = Math.floor(sortedSaliences.length / 2);
552
+ const medianSalience = sortedSaliences.length > 0 ? sortedSaliences.length % 2 !== 0 ? sortedSaliences[mid] ?? 0 : ((sortedSaliences[mid - 1] ?? 0) + (sortedSaliences[mid] ?? 0)) / 2 : 0;
553
+ const maxSalience = sortedSaliences.length > 0 ? sortedSaliences[sortedSaliences.length - 1] ?? 0 : 0;
554
+ const minSalience = sortedSaliences.length > 0 ? sortedSaliences[0] ?? 0 : 0;
555
+ return {
556
+ paths: rankedPaths,
557
+ stats: {
558
+ pathsRanked: rankedPaths.length,
559
+ meanSalience,
560
+ medianSalience,
561
+ maxSalience,
562
+ minSalience,
563
+ durationMs: endTime - startTime
564
+ }
565
+ };
566
+ }
567
+ /**
568
+ * Compute salience for a single path.
569
+ *
570
+ * Uses geometric mean of edge MI scores for length-unbiased ranking.
571
+ */
572
+ function computePathSalience(graph, path, mi, epsilon) {
573
+ const nodes = path.nodes;
574
+ if (nodes.length < 2) return epsilon;
575
+ let productMi = 1;
576
+ let edgeCount = 0;
577
+ for (let i = 0; i < nodes.length - 1; i++) {
578
+ const source = nodes[i];
579
+ const target = nodes[i + 1];
580
+ if (source !== void 0 && target !== void 0) {
581
+ const edgeMi = mi(graph, source, target);
582
+ productMi *= Math.max(epsilon, edgeMi);
583
+ edgeCount++;
584
+ }
585
+ }
586
+ if (edgeCount === 0) return epsilon;
587
+ const salience = Math.pow(productMi, 1 / edgeCount);
588
+ return Math.max(epsilon, Math.min(1, salience));
589
+ }
590
+ //#endregion
591
+ //#region src/ranking/mi/adamic-adar.ts
592
+ /**
593
+ * Compute Adamic-Adar index between neighbourhoods of two nodes.
594
+ *
595
+ * @param graph - Source graph
596
+ * @param source - Source node ID
597
+ * @param target - Target node ID
598
+ * @param config - Optional configuration
599
+ * @returns Adamic-Adar index (normalised to [0, 1] if configured)
600
+ */
601
+ function adamicAdar(graph, source, target, config) {
602
+ const { epsilon = 1e-10, normalise = true } = config ?? {};
603
+ const sourceNeighbours = new Set(graph.neighbours(source));
604
+ const targetNeighbours = new Set(graph.neighbours(target));
605
+ sourceNeighbours.delete(target);
606
+ targetNeighbours.delete(source);
607
+ let score = 0;
608
+ for (const neighbour of sourceNeighbours) if (targetNeighbours.has(neighbour)) {
609
+ const degree = graph.degree(neighbour);
610
+ if (degree > 1) score += 1 / Math.log(degree);
611
+ }
612
+ if (normalise) {
613
+ const commonCount = sourceNeighbours.size < targetNeighbours.size ? sourceNeighbours.size : targetNeighbours.size;
614
+ if (commonCount === 0) return 0;
615
+ const maxScore = commonCount / Math.log(2);
616
+ score = score / maxScore;
617
+ }
618
+ return Math.max(epsilon, score);
619
+ }
620
+ //#endregion
621
+ //#region src/ranking/mi/scale.ts
622
+ /**
623
+ * Compute SCALE MI between two nodes.
624
+ */
625
+ function scale(graph, source, target, config) {
626
+ const { epsilon = 1e-10 } = config ?? {};
627
+ const sourceNeighbours = new Set(graph.neighbours(source));
628
+ const targetNeighbours = new Set(graph.neighbours(target));
629
+ sourceNeighbours.delete(target);
630
+ targetNeighbours.delete(source);
631
+ const sourceDegree = sourceNeighbours.size;
632
+ const targetDegree = targetNeighbours.size;
633
+ let intersectionSize = 0;
634
+ for (const neighbour of sourceNeighbours) if (targetNeighbours.has(neighbour)) intersectionSize++;
635
+ const unionSize = sourceDegree + targetDegree - intersectionSize;
636
+ const jaccard = unionSize > 0 ? intersectionSize / unionSize : 0;
637
+ const minDegree = Math.min(sourceDegree, targetDegree);
638
+ const maxDegree = Math.max(sourceDegree, targetDegree);
639
+ const degreeRatio = maxDegree > 0 ? minDegree / maxDegree : 0;
640
+ if (jaccard + degreeRatio === 0) return epsilon;
641
+ const score = 2 * jaccard * degreeRatio / (jaccard + degreeRatio);
642
+ return Math.max(epsilon, Math.min(1, score));
643
+ }
644
+ //#endregion
645
+ //#region src/ranking/mi/skew.ts
646
+ /**
647
+ * Compute SKEW MI between two nodes.
648
+ */
649
+ function skew(graph, source, target, config) {
650
+ const { epsilon = 1e-10 } = config ?? {};
651
+ const sourceNeighbours = new Set(graph.neighbours(source));
652
+ const targetNeighbours = new Set(graph.neighbours(target));
653
+ sourceNeighbours.delete(target);
654
+ targetNeighbours.delete(source);
655
+ let weightedIntersection = 0;
656
+ let commonCount = 0;
657
+ for (const neighbour of sourceNeighbours) if (targetNeighbours.has(neighbour)) {
658
+ commonCount++;
659
+ const degree = graph.degree(neighbour);
660
+ if (degree > 1) weightedIntersection += 1 / Math.log(degree);
661
+ }
662
+ if (commonCount === 0) return epsilon;
663
+ const sourceDegree = sourceNeighbours.size;
664
+ const targetDegree = targetNeighbours.size;
665
+ const maxScore = Math.min(sourceDegree, targetDegree) / Math.log(2);
666
+ const score = weightedIntersection / maxScore;
667
+ return Math.max(epsilon, Math.min(1, score));
668
+ }
669
+ //#endregion
670
+ //#region src/ranking/mi/span.ts
671
+ /**
672
+ * Compute SPAN MI between two nodes.
673
+ */
674
+ function span(graph, source, target, config) {
675
+ const { epsilon = 1e-10 } = config ?? {};
676
+ const sourceNeighbours = new Set(graph.neighbours(source));
677
+ const targetNeighbours = new Set(graph.neighbours(target));
678
+ sourceNeighbours.delete(target);
679
+ targetNeighbours.delete(source);
680
+ const sourceDegree = sourceNeighbours.size;
681
+ const targetDegree = targetNeighbours.size;
682
+ let intersectionSize = 0;
683
+ for (const neighbour of sourceNeighbours) if (targetNeighbours.has(neighbour)) intersectionSize++;
684
+ const unionSize = sourceDegree + targetDegree - intersectionSize;
685
+ const jaccard = unionSize > 0 ? intersectionSize / unionSize : 0;
686
+ const maxDegree = Math.max(sourceDegree, targetDegree);
687
+ const degreeDiff = Math.abs(sourceDegree - targetDegree);
688
+ const degreeSimilarity = maxDegree > 0 ? 1 - degreeDiff / maxDegree : 1;
689
+ const score = Math.sqrt(jaccard * degreeSimilarity);
690
+ return Math.max(epsilon, Math.min(1, score));
691
+ }
692
+ //#endregion
693
+ //#region src/ranking/mi/etch.ts
694
+ /**
695
+ * Compute ETCH MI between two nodes.
696
+ */
697
+ function etch(graph, source, target, config) {
698
+ const { epsilon = 1e-10 } = config ?? {};
699
+ const sourceNeighbours = new Set(graph.neighbours(source));
700
+ const targetNeighbours = new Set(graph.neighbours(target));
701
+ sourceNeighbours.delete(target);
702
+ targetNeighbours.delete(source);
703
+ const commonNeighbours = [];
704
+ for (const neighbour of sourceNeighbours) if (targetNeighbours.has(neighbour)) commonNeighbours.push(neighbour);
705
+ if (commonNeighbours.length < 2) return epsilon;
706
+ let jointEdges = 0;
707
+ for (let i = 0; i < commonNeighbours.length; i++) for (let j = i + 1; j < commonNeighbours.length; j++) {
708
+ const ni = commonNeighbours[i];
709
+ const nj = commonNeighbours[j];
710
+ if (ni !== void 0 && nj !== void 0 && graph.getEdge(ni, nj) !== void 0) jointEdges++;
711
+ }
712
+ const maxJointEdges = commonNeighbours.length * (commonNeighbours.length - 1) / 2;
713
+ const jointDensity = maxJointEdges > 0 ? jointEdges / maxJointEdges : 0;
714
+ let commonEdges = 0;
715
+ for (const cn of commonNeighbours) {
716
+ if (graph.getEdge(source, cn) !== void 0) commonEdges++;
717
+ if (graph.getEdge(target, cn) !== void 0) commonEdges++;
718
+ }
719
+ const maxCommonEdges = commonNeighbours.length * 2;
720
+ const commonDensity = maxCommonEdges > 0 ? commonEdges / maxCommonEdges : 0;
721
+ const score = jointDensity * .7 + commonDensity * .3;
722
+ return Math.max(epsilon, Math.min(1, score));
723
+ }
724
+ //#endregion
725
+ //#region src/ranking/mi/notch.ts
726
+ /**
727
+ * Compute NOTCH MI between two nodes.
728
+ */
729
+ function notch(graph, source, target, config) {
730
+ const { epsilon = 1e-10 } = config ?? {};
731
+ const sourceNeighbours = new Set(graph.neighbours(source));
732
+ const targetNeighbours = new Set(graph.neighbours(target));
733
+ sourceNeighbours.delete(target);
734
+ targetNeighbours.delete(source);
735
+ const sourceDegree = sourceNeighbours.size;
736
+ const targetDegree = targetNeighbours.size;
737
+ let intersectionSize = 0;
738
+ for (const neighbour of sourceNeighbours) if (targetNeighbours.has(neighbour)) intersectionSize++;
739
+ const minDegree = Math.min(sourceDegree, targetDegree);
740
+ const overlap = minDegree > 0 ? intersectionSize / minDegree : 0;
741
+ const maxDegree = Math.max(sourceDegree, targetDegree);
742
+ const correlation = maxDegree > 0 ? 1 - Math.abs(sourceDegree - targetDegree) / maxDegree : 1;
743
+ const score = overlap * .6 + correlation * .4;
744
+ return Math.max(epsilon, Math.min(1, score));
745
+ }
746
+ //#endregion
747
+ //#region src/ranking/mi/adaptive.ts
748
+ /**
749
+ * Compute unified adaptive MI between two connected nodes.
750
+ *
751
+ * Combines structural, degree, and overlap signals with
752
+ * adaptive weighting based on graph density.
753
+ *
754
+ * @param graph - Source graph
755
+ * @param source - Source node ID
756
+ * @param target - Target node ID
757
+ * @param config - Optional configuration with component weights
758
+ * @returns Adaptive MI score in [0, 1]
759
+ */
760
+ function adaptive(graph, source, target, config) {
761
+ const { epsilon = 1e-10, structuralWeight = .4, degreeWeight = .3, overlapWeight = .3 } = config ?? {};
762
+ const structural = jaccard(graph, source, target, { epsilon });
763
+ const degreeComponent = adamicAdar(graph, source, target, {
764
+ epsilon,
765
+ normalise: true
766
+ });
767
+ const sourceNeighbours = new Set(graph.neighbours(source));
768
+ const targetNeighbours = new Set(graph.neighbours(target));
769
+ sourceNeighbours.delete(target);
770
+ targetNeighbours.delete(source);
771
+ const sourceDegree = sourceNeighbours.size;
772
+ const targetDegree = targetNeighbours.size;
773
+ let overlap;
774
+ if (sourceDegree > 0 && targetDegree > 0) {
775
+ let commonCount = 0;
776
+ for (const n of sourceNeighbours) if (targetNeighbours.has(n)) commonCount++;
777
+ overlap = commonCount / Math.min(sourceDegree, targetDegree);
778
+ } else overlap = epsilon;
779
+ const totalWeight = structuralWeight + degreeWeight + overlapWeight;
780
+ const score = (structuralWeight * structural + degreeWeight * degreeComponent + overlapWeight * overlap) / totalWeight;
781
+ return Math.max(epsilon, Math.min(1, score));
782
+ }
783
+ //#endregion
784
+ //#region src/ranking/baselines/shortest.ts
785
+ /**
786
+ * Rank paths by length (shortest first).
787
+ *
788
+ * Score = 1 / path_length, normalised to [0, 1].
789
+ *
790
+ * @param _graph - Source graph (unused for length ranking)
791
+ * @param paths - Paths to rank
792
+ * @param config - Configuration options
793
+ * @returns Ranked paths (shortest first)
794
+ */
795
+ function shortest(_graph, paths, config) {
796
+ const { includeScores = true } = config ?? {};
797
+ if (paths.length === 0) return {
798
+ paths: [],
799
+ method: "shortest"
800
+ };
801
+ const scored = paths.map((path) => ({
802
+ path,
803
+ score: 1 / path.nodes.length
804
+ }));
805
+ const maxScore = Math.max(...scored.map((s) => s.score));
806
+ return {
807
+ paths: scored.map(({ path, score }) => ({
808
+ ...path,
809
+ score: includeScores ? score / maxScore : score / maxScore
810
+ })).sort((a, b) => b.score - a.score),
811
+ method: "shortest"
812
+ };
813
+ }
814
+ //#endregion
815
+ //#region src/extraction/ego-network.ts
816
+ /**
817
+ * Extract the ego-network (k-hop neighbourhood) of a centre node.
818
+ *
819
+ * The ego-network includes all nodes reachable within k hops from the
820
+ * centre node, plus all edges between those nodes (induced subgraph).
821
+ *
822
+ * For directed graphs, the search follows outgoing edges by default.
823
+ * To include incoming edges, use direction 'both' in the underlying traversal.
824
+ *
825
+ * @param graph - The source graph
826
+ * @param centre - The centre node ID
827
+ * @param options - Extraction options
828
+ * @returns An induced subgraph of the k-hop neighbourhood
829
+ * @throws Error if the centre node does not exist in the graph
830
+ *
831
+ * @example
832
+ * ```typescript
833
+ * // 2-hop neighbourhood
834
+ * const ego = extractEgoNetwork(graph, 'A', { hops: 2 });
835
+ * ```
836
+ */
837
+ function extractEgoNetwork(graph, centre, options) {
838
+ const hops = options?.hops ?? 1;
839
+ if (!graph.hasNode(centre)) throw new Error(`Centre node '${centre}' does not exist in the graph`);
840
+ if (hops < 0) throw new Error(`Hops must be non-negative, got ${String(hops)}`);
841
+ const nodesInEgoNetwork = new Set([centre]);
842
+ if (hops > 0) {
843
+ const visited = new Set([centre]);
844
+ const queue = [[centre, 0]];
845
+ while (queue.length > 0) {
846
+ const entry = queue.shift();
847
+ if (entry === void 0) break;
848
+ const [current, distance] = entry;
849
+ if (distance < hops) {
850
+ for (const neighbour of graph.neighbours(current)) if (!visited.has(neighbour)) {
851
+ visited.add(neighbour);
852
+ nodesInEgoNetwork.add(neighbour);
853
+ queue.push([neighbour, distance + 1]);
854
+ }
855
+ }
856
+ }
857
+ }
858
+ const result = graph.directed ? AdjacencyMapGraph.directed() : AdjacencyMapGraph.undirected();
859
+ for (const nodeId of nodesInEgoNetwork) {
860
+ const nodeData = graph.getNode(nodeId);
861
+ if (nodeData !== void 0) result.addNode(nodeData);
862
+ }
863
+ for (const edge of graph.edges()) if (nodesInEgoNetwork.has(edge.source) && nodesInEgoNetwork.has(edge.target)) result.addEdge(edge);
864
+ return result;
865
+ }
866
+ //#endregion
867
+ //#region src/extraction/k-core.ts
868
+ /**
869
+ * Extract the k-core of a graph.
870
+ *
871
+ * The k-core is the maximal connected subgraph where every node has
872
+ * degree at least k. This is computed using a peeling algorithm that
873
+ * iteratively removes nodes with degree less than k.
874
+ *
875
+ * For undirected graphs, degree counts all adjacent nodes.
876
+ * For directed graphs, degree counts both in- and out-neighbours.
877
+ *
878
+ * @param graph - The source graph
879
+ * @param k - The minimum degree threshold
880
+ * @returns A new graph containing the k-core (may be empty)
881
+ *
882
+ * @example
883
+ * ```typescript
884
+ * // Extract the 3-core (nodes with at least 3 neighbours)
885
+ * const core3 = extractKCore(graph, 3);
886
+ * ```
887
+ */
888
+ function extractKCore(graph, k) {
889
+ if (k < 0) throw new Error(`k must be non-negative, got ${String(k)}`);
890
+ const remaining = /* @__PURE__ */ new Set();
891
+ const degrees = /* @__PURE__ */ new Map();
892
+ for (const nodeId of graph.nodeIds()) {
893
+ remaining.add(nodeId);
894
+ const deg = graph.directed ? graph.degree(nodeId, "both") : graph.degree(nodeId);
895
+ degrees.set(nodeId, deg);
896
+ }
897
+ const toRemove = [];
898
+ for (const [nodeId, deg] of degrees) if (deg < k) toRemove.push(nodeId);
899
+ while (toRemove.length > 0) {
900
+ const nodeId = toRemove.shift();
901
+ if (nodeId === void 0) break;
902
+ if (!remaining.has(nodeId)) continue;
903
+ remaining.delete(nodeId);
904
+ const neighbours = graph.directed ? graph.neighbours(nodeId, "both") : graph.neighbours(nodeId);
905
+ for (const neighbour of neighbours) if (remaining.has(neighbour)) {
906
+ const newDeg = (degrees.get(neighbour) ?? 0) - 1;
907
+ degrees.set(neighbour, newDeg);
908
+ if (newDeg < k && newDeg === k - 1) toRemove.push(neighbour);
909
+ }
910
+ }
911
+ const result = graph.directed ? AdjacencyMapGraph.directed() : AdjacencyMapGraph.undirected();
912
+ for (const nodeId of remaining) {
913
+ const nodeData = graph.getNode(nodeId);
914
+ if (nodeData !== void 0) result.addNode(nodeData);
915
+ }
916
+ for (const edge of graph.edges()) if (remaining.has(edge.source) && remaining.has(edge.target)) result.addEdge(edge);
917
+ return result;
918
+ }
919
+ //#endregion
920
+ //#region src/extraction/truss.ts
921
+ /**
922
+ * Count triangles involving a given edge.
923
+ *
924
+ * For an edge (u, v), count common neighbours of u and v.
925
+ * Each common neighbour w forms a triangle u-v-w.
926
+ *
927
+ * @param graph - The graph
928
+ * @param u - First endpoint
929
+ * @param v - Second endpoint
930
+ * @returns Number of triangles containing the edge (u, v)
931
+ */
932
+ function countEdgeTriangles(graph, u, v) {
933
+ const uNeighbours = new Set(graph.neighbours(u));
934
+ let count = 0;
935
+ for (const w of graph.neighbours(v)) if (w !== u && uNeighbours.has(w)) count++;
936
+ return count;
937
+ }
938
+ /**
939
+ * Extract the k-truss of a graph.
940
+ *
941
+ * The k-truss is the maximal subgraph where every edge participates in
942
+ * at least k-2 triangles. This is computed by iteratively removing edges
943
+ * with fewer than k-2 triangles, then removing isolated nodes.
944
+ *
945
+ * Note: K-truss is typically defined for undirected graphs. For directed
946
+ * graphs, this treats the graph as undirected for triangle counting.
947
+ *
948
+ * @param graph - The source graph
949
+ * @param k - The minimum triangle count threshold (edge must be in >= k-2 triangles)
950
+ * @returns A new graph containing the k-truss (may be empty)
951
+ *
952
+ * @example
953
+ * ```typescript
954
+ * // Extract the 3-truss (edges in at least 1 triangle)
955
+ * const truss3 = extractKTruss(graph, 3);
956
+ * ```
957
+ */
958
+ function extractKTruss(graph, k) {
959
+ if (k < 2) throw new Error(`k must be at least 2, got ${String(k)}`);
960
+ const minTriangles = k - 2;
961
+ const adjacency = /* @__PURE__ */ new Map();
962
+ const edgeData = /* @__PURE__ */ new Map();
963
+ const remainingEdges = /* @__PURE__ */ new Set();
964
+ for (const nodeId of graph.nodeIds()) adjacency.set(nodeId, /* @__PURE__ */ new Set());
965
+ for (const edge of graph.edges()) {
966
+ const { source, target } = edge;
967
+ adjacency.get(source)?.add(target);
968
+ adjacency.get(target)?.add(source);
969
+ const key = source < target ? `${source}::${target}` : `${target}::${source}`;
970
+ edgeData.set(key, edge);
971
+ remainingEdges.add(key);
972
+ }
973
+ const triangleCounts = /* @__PURE__ */ new Map();
974
+ const edgesToRemove = [];
975
+ for (const key of remainingEdges) {
976
+ const edge = edgeData.get(key);
977
+ if (edge !== void 0) {
978
+ const count = countEdgeTriangles(graph, edge.source, edge.target);
979
+ triangleCounts.set(key, count);
980
+ if (count < minTriangles) edgesToRemove.push(key);
981
+ }
982
+ }
983
+ while (edgesToRemove.length > 0) {
984
+ const edgeKey = edgesToRemove.shift();
985
+ if (edgeKey === void 0) break;
986
+ if (!remainingEdges.has(edgeKey)) continue;
987
+ remainingEdges.delete(edgeKey);
988
+ const edge = edgeData.get(edgeKey);
989
+ if (edge === void 0) continue;
990
+ const { source, target } = edge;
991
+ adjacency.get(source)?.delete(target);
992
+ adjacency.get(target)?.delete(source);
993
+ const sourceNeighbours = adjacency.get(source);
994
+ if (sourceNeighbours !== void 0) {
995
+ for (const w of adjacency.get(target) ?? []) if (sourceNeighbours.has(w)) {
996
+ const keySw = source < w ? `${source}::${w}` : `${w}::${source}`;
997
+ const keyTw = target < w ? `${target}::${w}` : `${w}::${target}`;
998
+ for (const keyToUpdate of [keySw, keyTw]) if (remainingEdges.has(keyToUpdate)) {
999
+ const newCount = (triangleCounts.get(keyToUpdate) ?? 0) - 1;
1000
+ triangleCounts.set(keyToUpdate, newCount);
1001
+ if (newCount < minTriangles && newCount === minTriangles - 1) edgesToRemove.push(keyToUpdate);
1002
+ }
1003
+ }
1004
+ }
1005
+ }
1006
+ const nodesWithEdges = /* @__PURE__ */ new Set();
1007
+ for (const key of remainingEdges) {
1008
+ const edge = edgeData.get(key);
1009
+ if (edge !== void 0) {
1010
+ nodesWithEdges.add(edge.source);
1011
+ nodesWithEdges.add(edge.target);
1012
+ }
1013
+ }
1014
+ const result = graph.directed ? AdjacencyMapGraph.directed() : AdjacencyMapGraph.undirected();
1015
+ for (const nodeId of nodesWithEdges) {
1016
+ const nodeData = graph.getNode(nodeId);
1017
+ if (nodeData !== void 0) result.addNode(nodeData);
1018
+ }
1019
+ for (const key of remainingEdges) {
1020
+ const edge = edgeData.get(key);
1021
+ if (edge !== void 0 && result.hasNode(edge.source) && result.hasNode(edge.target)) result.addEdge(edge);
1022
+ }
1023
+ return result;
1024
+ }
1025
+ //#endregion
1026
+ //#region src/extraction/motif.ts
1027
+ /**
1028
+ * Canonicalise an edge pattern for hashing.
1029
+ *
1030
+ * Returns a canonical string representation of a small graph pattern.
1031
+ */
1032
+ function canonicalisePattern(nodeCount, edges) {
1033
+ const permutations = getPermutations(nodeCount);
1034
+ let minPattern = null;
1035
+ for (const perm of permutations) {
1036
+ const transformedEdges = edges.map(([u, v]) => {
1037
+ const pu = perm[u] ?? -1;
1038
+ const pv = perm[v] ?? -1;
1039
+ if (pu < 0 || pv < 0) return;
1040
+ return pu < pv ? `${String(pu)}-${String(pv)}` : `${String(pv)}-${String(pu)}`;
1041
+ }).filter((edge) => edge !== void 0).sort().join(",");
1042
+ if (minPattern === null || transformedEdges < minPattern) minPattern = transformedEdges;
1043
+ }
1044
+ return minPattern ?? "";
1045
+ }
1046
+ /**
1047
+ * Generate all permutations of [0, n-1].
1048
+ */
1049
+ function getPermutations(n) {
1050
+ if (n === 0) return [[]];
1051
+ if (n === 1) return [[0]];
1052
+ const result = [];
1053
+ const arr = Array.from({ length: n }, (_, i) => i);
1054
+ function permute(start) {
1055
+ if (start === n - 1) {
1056
+ result.push([...arr]);
1057
+ return;
1058
+ }
1059
+ for (let i = start; i < n; i++) {
1060
+ const startVal = arr[start];
1061
+ const iVal = arr[i];
1062
+ if (startVal === void 0 || iVal === void 0) continue;
1063
+ arr[start] = iVal;
1064
+ arr[i] = startVal;
1065
+ permute(start + 1);
1066
+ arr[start] = startVal;
1067
+ arr[i] = iVal;
1068
+ }
1069
+ }
1070
+ permute(0);
1071
+ return result;
1072
+ }
1073
+ /**
1074
+ * Enumerate all 3-node motifs in the graph.
1075
+ *
1076
+ * A 3-node motif (triad) can be one of 4 isomorphism classes for undirected graphs:
1077
+ * - Empty: no edges
1078
+ * - 1-edge: single edge
1079
+ * - 2-star: two edges sharing a node (path of length 2)
1080
+ * - Triangle: three edges (complete graph K3)
1081
+ *
1082
+ * For directed graphs, there are 16 isomorphism classes.
1083
+ *
1084
+ * @param graph - The source graph
1085
+ * @param includeInstances - Whether to include node instances in the result
1086
+ * @returns Motif census with counts and optionally instances
1087
+ */
1088
+ function enumerate3NodeMotifs(graph, includeInstances) {
1089
+ const counts = /* @__PURE__ */ new Map();
1090
+ const instances = includeInstances ? /* @__PURE__ */ new Map() : void 0;
1091
+ const nodeList = [...graph.nodeIds()];
1092
+ const n = nodeList.length;
1093
+ for (let i = 0; i < n; i++) {
1094
+ const ni = nodeList[i];
1095
+ if (ni === void 0) continue;
1096
+ for (let j = i + 1; j < n; j++) {
1097
+ const nj = nodeList[j];
1098
+ if (nj === void 0) continue;
1099
+ for (let k = j + 1; k < n; k++) {
1100
+ const nk = nodeList[k];
1101
+ if (nk === void 0) continue;
1102
+ const nodes = [
1103
+ ni,
1104
+ nj,
1105
+ nk
1106
+ ];
1107
+ const edges = [];
1108
+ for (const [u, v] of [
1109
+ [0, 1],
1110
+ [0, 2],
1111
+ [1, 2]
1112
+ ]) {
1113
+ const nu = nodes[u];
1114
+ const nv = nodes[v];
1115
+ if (nu === void 0 || nv === void 0) continue;
1116
+ if (graph.getEdge(nu, nv) !== void 0) edges.push([u, v]);
1117
+ else if (!graph.directed && graph.getEdge(nv, nu) !== void 0) edges.push([u, v]);
1118
+ else if (graph.directed && graph.getEdge(nv, nu) !== void 0) edges.push([v, u]);
1119
+ }
1120
+ const pattern = canonicalisePattern(3, edges);
1121
+ const count = counts.get(pattern) ?? 0;
1122
+ counts.set(pattern, count + 1);
1123
+ if (includeInstances && instances !== void 0) {
1124
+ if (!instances.has(pattern)) instances.set(pattern, []);
1125
+ const patternInstances = instances.get(pattern);
1126
+ if (patternInstances !== void 0) patternInstances.push([
1127
+ ni,
1128
+ nj,
1129
+ nk
1130
+ ]);
1131
+ }
1132
+ }
1133
+ }
1134
+ }
1135
+ return {
1136
+ counts,
1137
+ instances
1138
+ };
1139
+ }
1140
+ /**
1141
+ * Enumerate all 4-node motifs in the graph.
1142
+ *
1143
+ * A 4-node motif can be one of 11 isomorphism classes for undirected graphs
1144
+ * (ranging from empty to complete K4), or many more for directed graphs.
1145
+ *
1146
+ * @param graph - The source graph
1147
+ * @param includeInstances - Whether to include node instances in the result
1148
+ * @returns Motif census with counts and optionally instances
1149
+ */
1150
+ function enumerate4NodeMotifs(graph, includeInstances) {
1151
+ const counts = /* @__PURE__ */ new Map();
1152
+ const instances = includeInstances ? /* @__PURE__ */ new Map() : void 0;
1153
+ const nodeList = [...graph.nodeIds()];
1154
+ const n = nodeList.length;
1155
+ for (let i = 0; i < n; i++) {
1156
+ const ni = nodeList[i];
1157
+ if (ni === void 0) continue;
1158
+ for (let j = i + 1; j < n; j++) {
1159
+ const nj = nodeList[j];
1160
+ if (nj === void 0) continue;
1161
+ for (let k = j + 1; k < n; k++) {
1162
+ const nk = nodeList[k];
1163
+ if (nk === void 0) continue;
1164
+ for (let l = k + 1; l < n; l++) {
1165
+ const nl = nodeList[l];
1166
+ if (nl === void 0) continue;
1167
+ const nodes = [
1168
+ ni,
1169
+ nj,
1170
+ nk,
1171
+ nl
1172
+ ];
1173
+ const edges = [];
1174
+ for (const [u, v] of [
1175
+ [0, 1],
1176
+ [0, 2],
1177
+ [0, 3],
1178
+ [1, 2],
1179
+ [1, 3],
1180
+ [2, 3]
1181
+ ]) {
1182
+ const nu = nodes[u];
1183
+ const nv = nodes[v];
1184
+ if (nu === void 0 || nv === void 0) continue;
1185
+ if (graph.getEdge(nu, nv) !== void 0) edges.push([u, v]);
1186
+ else if (!graph.directed && graph.getEdge(nv, nu) !== void 0) edges.push([u, v]);
1187
+ else if (graph.directed && graph.getEdge(nv, nu) !== void 0) edges.push([v, u]);
1188
+ }
1189
+ const pattern = canonicalisePattern(4, edges);
1190
+ const count = counts.get(pattern) ?? 0;
1191
+ counts.set(pattern, count + 1);
1192
+ if (includeInstances && instances !== void 0) {
1193
+ if (!instances.has(pattern)) instances.set(pattern, []);
1194
+ const patternInstances = instances.get(pattern);
1195
+ if (patternInstances !== void 0) patternInstances.push([
1196
+ ni,
1197
+ nj,
1198
+ nk,
1199
+ nl
1200
+ ]);
1201
+ }
1202
+ }
1203
+ }
1204
+ }
1205
+ }
1206
+ return {
1207
+ counts,
1208
+ instances
1209
+ };
1210
+ }
1211
+ /**
1212
+ * Human-readable names for common 3-node motifs.
1213
+ */
1214
+ var MOTIF_3_NAMES = new Map([
1215
+ ["", "empty"],
1216
+ ["0-1", "1-edge"],
1217
+ ["0-1,0-2", "2-star"],
1218
+ ["0-1,1-2", "path-3"],
1219
+ ["0-1,0-2,1-2", "triangle"]
1220
+ ]);
1221
+ /**
1222
+ * Human-readable names for common 4-node motifs.
1223
+ */
1224
+ var MOTIF_4_NAMES = new Map([
1225
+ ["", "empty"],
1226
+ ["0-1", "1-edge"],
1227
+ ["0-1,0-2", "2-star"],
1228
+ ["0-1,0-2,0-3", "3-star"],
1229
+ ["0-1,0-2,1-2", "triangle"],
1230
+ ["0-1,0-2,1-2,2-3", "paw"],
1231
+ ["0-1,0-2,2-3", "path-4"],
1232
+ ["0-1,0-2,1-3,2-3", "4-cycle"],
1233
+ ["0-1,0-2,1-2,0-3,1-3", "diamond"],
1234
+ ["0-1,0-2,0-3,1-2,1-3,2-3", "K4"]
1235
+ ]);
1236
+ /**
1237
+ * Enumerate motifs of a given size in the graph.
1238
+ *
1239
+ * This function counts all occurrences of each distinct motif type
1240
+ * (isomorphism class) in the graph. For graphs with many nodes,
1241
+ * 4-motif enumeration can be expensive (O(n^4) worst case).
1242
+ *
1243
+ * @param graph - The source graph
1244
+ * @param size - Motif size (3 or 4 nodes)
1245
+ * @returns Motif census with counts per motif type
1246
+ *
1247
+ * @example
1248
+ * ```typescript
1249
+ * // Count all triangles and other 3-node patterns
1250
+ * const census3 = enumerateMotifs(graph, 3);
1251
+ * console.log(`Triangles: ${census3.counts.get('0-1,0-2,1-2')}`);
1252
+ *
1253
+ * // Count 4-node patterns
1254
+ * const census4 = enumerateMotifs(graph, 4);
1255
+ * ```
1256
+ */
1257
+ function enumerateMotifs(graph, size) {
1258
+ return size === 3 ? enumerate3NodeMotifs(graph, false) : enumerate4NodeMotifs(graph, false);
1259
+ }
1260
+ /**
1261
+ * Enumerate motifs with optional instance tracking.
1262
+ *
1263
+ * @param graph - The source graph
1264
+ * @param size - Motif size (3 or 4 nodes)
1265
+ * @param includeInstances - Whether to include node instances
1266
+ * @returns Motif census with counts and optionally instances
1267
+ */
1268
+ function enumerateMotifsWithInstances(graph, size, includeInstances) {
1269
+ return size === 3 ? enumerate3NodeMotifs(graph, includeInstances) : enumerate4NodeMotifs(graph, includeInstances);
1270
+ }
1271
+ /**
1272
+ * Get a human-readable name for a motif pattern.
1273
+ *
1274
+ * @param pattern - The canonical pattern string
1275
+ * @param size - Motif size (3 or 4 nodes)
1276
+ * @returns A human-readable name, or the pattern itself if unknown
1277
+ */
1278
+ function getMotifName(pattern, size) {
1279
+ return (size === 3 ? MOTIF_3_NAMES : MOTIF_4_NAMES).get(pattern) ?? pattern;
1280
+ }
1281
+ //#endregion
1282
+ //#region src/extraction/induced-subgraph.ts
1283
+ /**
1284
+ * Extract the induced subgraph containing exactly the specified nodes.
1285
+ *
1286
+ * The induced subgraph includes all nodes from the input set that exist
1287
+ * in the original graph, plus all edges where both endpoints are in the set.
1288
+ *
1289
+ * @param graph - The source graph
1290
+ * @param nodes - Set of node IDs to include in the subgraph
1291
+ * @returns A new graph containing the induced subgraph
1292
+ *
1293
+ * @example
1294
+ * ```typescript
1295
+ * const subgraph = extractInducedSubgraph(graph, new Set(['A', 'B', 'C']));
1296
+ * ```
1297
+ */
1298
+ function extractInducedSubgraph(graph, nodes) {
1299
+ const result = graph.directed ? AdjacencyMapGraph.directed() : AdjacencyMapGraph.undirected();
1300
+ for (const nodeId of nodes) {
1301
+ const nodeData = graph.getNode(nodeId);
1302
+ if (nodeData !== void 0) result.addNode(nodeData);
1303
+ }
1304
+ for (const edge of graph.edges()) if (result.hasNode(edge.source) && result.hasNode(edge.target)) result.addEdge(edge);
1305
+ return result;
1306
+ }
1307
+ //#endregion
1308
+ //#region src/extraction/node-filter.ts
1309
+ /**
1310
+ * Extract a filtered subgraph based on node and edge predicates.
1311
+ *
1312
+ * Nodes are first filtered by the node predicate (if provided).
1313
+ * Edges are then filtered by the edge predicate (if provided), and only
1314
+ * retained if both endpoints pass the node predicate.
1315
+ *
1316
+ * @param graph - The source graph
1317
+ * @param options - Filter options specifying node/edge predicates
1318
+ * @returns A new graph containing only nodes and edges that pass the predicates
1319
+ *
1320
+ * @example
1321
+ * ```typescript
1322
+ * // Extract subgraph of high-weight nodes
1323
+ * const filtered = filterSubgraph(graph, {
1324
+ * nodePredicate: (node) => (node.weight ?? 0) > 0.5,
1325
+ * removeIsolated: true
1326
+ * });
1327
+ * ```
1328
+ */
1329
+ function filterSubgraph(graph, options) {
1330
+ const { nodePredicate, edgePredicate, removeIsolated = false } = options ?? {};
1331
+ const result = graph.directed ? AdjacencyMapGraph.directed() : AdjacencyMapGraph.undirected();
1332
+ const includedNodes = /* @__PURE__ */ new Set();
1333
+ for (const nodeId of graph.nodeIds()) {
1334
+ const nodeData = graph.getNode(nodeId);
1335
+ if (nodeData !== void 0) {
1336
+ if (nodePredicate === void 0 || nodePredicate(nodeData)) {
1337
+ result.addNode(nodeData);
1338
+ includedNodes.add(nodeId);
1339
+ }
1340
+ }
1341
+ }
1342
+ for (const edge of graph.edges()) {
1343
+ if (!includedNodes.has(edge.source) || !includedNodes.has(edge.target)) continue;
1344
+ if (edgePredicate === void 0 || edgePredicate(edge)) result.addEdge(edge);
1345
+ }
1346
+ if (removeIsolated) {
1347
+ const isolatedNodes = [];
1348
+ for (const nodeId of result.nodeIds()) if (result.degree(nodeId) === 0) isolatedNodes.push(nodeId);
1349
+ for (const nodeId of isolatedNodes) result.removeNode(nodeId);
1350
+ }
1351
+ return result;
1352
+ }
1353
+ //#endregion
1354
+ export { AdjacencyMapGraph, GPUContext, PriorityQueue, adamicAdar, adaptive, approximateClusteringCoefficient, base, batchClusteringCoefficients, bfs, bfsWithPath, csrToGPUBuffers, detectWebGPU, dfs, dfsWithPath, dome, edge, entropyFromCounts, enumerateMotifs, enumerateMotifsWithInstances, etch, extractEgoNetwork, extractInducedSubgraph, extractKCore, extractKTruss, filterSubgraph, getMotifName, graphToCSR, grasp, hae, jaccard, localClusteringCoefficient, localTypeEntropy, maze, miniBatchKMeans, normaliseFeatures, normaliseFeatures as zScoreNormalise, normalisedEntropy, notch, parse, pipe, reach, sage, scale, shannonEntropy, shortest, skew, span, stratified };
1355
+
1356
+ //# sourceMappingURL=index.js.map