graphwise 1.4.0 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -81,25 +81,35 @@ where $H_{\text{local}}(v) = -\sum_{\tau} p(\tau) \log p(\tau)$ is the Shannon e
81
81
 
82
82
  #### PIPE: Path-potential Informed Priority Expansion
83
83
 
84
- $$\pi_{\text{PIPE}}(v) = \frac{\deg(v)}{1 + \text{path\_potential}(v)}$$
84
+ $$\pi_{\text{PIPE}}(v) = \frac{\deg(v)}{1 + \mathrm{pathPotential}(v)}$$
85
85
 
86
- where $\text{path\_potential}(v) = |N(v) \cap \bigcup_{j \neq i} V_j|$ counts neighbours already visited by other seed frontiers. High path potential indicates imminent path completion.
86
+ where $\mathrm{pathPotential}(v) = \lvert N(v) \cap \bigcup_{j \neq i} V_j \rvert$ counts neighbours already visited by other seed frontiers. High path potential indicates imminent path completion.
87
87
 
88
88
  #### SAGE: Salience-Accumulation Guided Expansion
89
89
 
90
- $$\pi_{\text{SAGE}}(v) = \begin{cases} \log(\deg(v) + 1) & \text{Phase 1 (before first path)} \\ -(\text{salience}(v) \times 1000 - \deg(v)) & \text{Phase 2 (after first path)} \end{cases}$$
90
+ $$
91
+ \pi_{\text{SAGE}}(v) = \begin{cases} \log(\deg(v) + 1) & \text{Phase 1 (before first path)} \\ -(\text{salience}(v) \times 1000 - \deg(v)) & \text{Phase 2 (after first path)} \end{cases}
92
+ $$
91
93
 
92
94
  where $\text{salience}(v)$ counts discovered paths containing $v$. Salience dominates in Phase 2; degree serves as tiebreaker.
93
95
 
94
96
  #### REACH: Retrospective Expansion with Adaptive Convergence
95
97
 
96
- $$\pi_{\text{REACH}}(v) = \begin{cases} \log(\deg(v) + 1) & \text{Phase 1} \\ \log(\deg(v) + 1) \times (1 - \widehat{\text{MI}}(v)) & \text{Phase 2} \end{cases}$$
98
+ $$
99
+ \pi_{\text{REACH}}(v) = \begin{cases} \log(\deg(v) + 1) & \text{Phase 1} \\ \log(\deg(v) + 1) \times (1 - \widehat{\text{MI}}(v)) & \text{Phase 2} \end{cases}
100
+ $$
97
101
 
98
- where $\widehat{\text{MI}}(v) = \frac{1}{|\mathcal{P}_{\text{top}}|} \sum_{p} J(N(v), N(p_{\text{endpoint}}))$ estimates MI via Jaccard similarity to discovered path endpoints.
102
+ where $\widehat{\text{MI}}(v)$ estimates MI via Jaccard similarity to discovered path endpoints:
103
+
104
+ $$
105
+ \widehat{\text{MI}}(v) = \frac{1}{\lvert \mathcal{P}\_{\text{top}} \rvert} \sum\_{p} J(N(v), N(p\_{\text{endpoint}}))
106
+ $$
99
107
 
100
108
  #### MAZE: Multi-frontier Adaptive Zone Expansion
101
109
 
102
- $$\pi^{(1)}(v) = \frac{\deg(v)}{1 + \text{path\_potential}(v)} \qquad \pi^{(2)}(v) = \pi^{(1)}(v) \times \frac{1}{1 + \lambda \cdot \text{salience}(v)}$$
110
+ $$
111
+ \pi^{(1)}(v) = \frac{\deg(v)}{1 + \mathrm{pathPotential}(v)} \qquad \pi^{(2)}(v) = \pi^{(1)}(v) \times \frac{1}{1 + \lambda \cdot \text{salience}(v)}
112
+ $$
103
113
 
104
114
  Phase 1 uses PIPE's path potential until $M$ paths found. Phase 2 incorporates SAGE's salience feedback. Phase 3 evaluates diversity, path count, and salience plateau for termination.
105
115
 
@@ -129,7 +139,9 @@ Single-phase weighted blend of degree and MI. Related to SAGE but uses continuou
129
139
 
130
140
  #### SIFT: Salience-Informed Frontier Threshold
131
141
 
132
- $$\pi_{\text{SIFT}}(v) = \begin{cases} 1 - \overline{\text{MI}} & \text{if } \overline{\text{MI}} \geq \tau \\ \deg(v) + 100 & \text{otherwise} \end{cases}$$
142
+ $$
143
+ \pi_{\text{SIFT}}(v) = \begin{cases} 1 - \overline{\text{MI}} & \text{if } \overline{\text{MI}} \geq \tau \\ \deg(v) + 100 & \text{otherwise} \end{cases}
144
+ $$
133
145
 
134
146
  MI-threshold-based priority with degree fallback. Related to REACH but uses a hard threshold instead of continuous MI-weighted priority.
135
147
 
@@ -215,7 +227,7 @@ where $c(\tau_u)$ is the count of nodes with the same type as $u$. Weights Jacca
215
227
  | --------------------------- | --------------------------------------------------- |
216
228
  | **Katz Index** | $\sum_{k=1}^{\infty} \beta^k (A^k)_{st}$ |
217
229
  | **Communicability** | $(e^A)_{st}$ |
218
- | **Resistance Distance** | $L^{+}_{ss} + L^{+}_{tt} - 2L^{+}_{st}$ |
230
+ | **Resistance Distance** | $L^{+}\_{ss} + L^{+}\_{tt} - 2L^{+}\_{st}$ |
219
231
  | **Jaccard Arithmetic Mean** | $\frac{1}{k} \sum J(N(u), N(v))$ |
220
232
  | **Degree-Sum** | $\sum_{v \in P} \deg(v)$ |
221
233
  | **Widest Path** | $\min_{(u,v) \in P} w(u,v)$ |
@@ -1 +1 @@
1
- {"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../src/expansion/base.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAU,QAAQ,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE1E,OAAO,KAAK,EACX,IAAI,EACJ,eAAe,EAGf,eAAe,EAEf,MAAM,SAAS,CAAC;AAqBjB;;;;;;;GAOG;AACH,wBAAgB,IAAI,CAAC,CAAC,SAAS,QAAQ,EAAE,CAAC,SAAS,QAAQ,EAC1D,KAAK,EAAE,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,EAC1B,KAAK,EAAE,SAAS,IAAI,EAAE,EACtB,MAAM,CAAC,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,GAC5B,eAAe,CAoOjB"}
1
+ {"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../src/expansion/base.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAU,QAAQ,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE1E,OAAO,KAAK,EACX,IAAI,EACJ,eAAe,EAGf,eAAe,EAEf,MAAM,SAAS,CAAC;AAqBjB;;;;;;;GAOG;AACH,wBAAgB,IAAI,CAAC,CAAC,SAAS,QAAQ,EAAE,CAAC,SAAS,QAAQ,EAC1D,KAAK,EAAE,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,EAC1B,KAAK,EAAE,SAAS,IAAI,EAAE,EACtB,MAAM,CAAC,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,GAC5B,eAAe,CAwOjB"}
@@ -1 +1 @@
1
- {"version":3,"file":"maze.d.ts","sourceRoot":"","sources":["../../src/expansion/maze.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,aAAa,EAAU,MAAM,UAAU,CAAC;AAC1E,OAAO,KAAK,EACX,IAAI,EACJ,eAAe,EACf,eAAe,EAEf,MAAM,SAAS,CAAC;AASjB;;;;;;;;;;GAUG;AACH,wBAAgB,IAAI,CAAC,CAAC,SAAS,QAAQ,EAAE,CAAC,SAAS,QAAQ,EAC1D,KAAK,EAAE,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,EAC1B,KAAK,EAAE,SAAS,IAAI,EAAE,EACtB,MAAM,CAAC,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,GAC5B,eAAe,CAwEjB"}
1
+ {"version":3,"file":"maze.d.ts","sourceRoot":"","sources":["../../src/expansion/maze.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,aAAa,EAAU,MAAM,UAAU,CAAC;AAC1E,OAAO,KAAK,EACX,IAAI,EACJ,eAAe,EACf,eAAe,EAEf,MAAM,SAAS,CAAC;AASjB;;;;;;;;;;GAUG;AACH,wBAAgB,IAAI,CAAC,CAAC,SAAS,QAAQ,EAAE,CAAC,SAAS,QAAQ,EAC1D,KAAK,EAAE,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,EAC1B,KAAK,EAAE,SAAS,IAAI,EAAE,EACtB,MAAM,CAAC,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,GAC5B,eAAe,CAsEjB"}
@@ -1 +1 @@
1
- {"version":3,"file":"reach.d.ts","sourceRoot":"","sources":["../../src/expansion/reach.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,aAAa,EAAU,MAAM,UAAU,CAAC;AAC1E,OAAO,KAAK,EACX,IAAI,EACJ,eAAe,EACf,eAAe,EAEf,MAAM,SAAS,CAAC;AAIjB;;;;;;;;;;;GAWG;AACH,wBAAgB,KAAK,CAAC,CAAC,SAAS,QAAQ,EAAE,CAAC,SAAS,QAAQ,EAC3D,KAAK,EAAE,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,EAC1B,KAAK,EAAE,SAAS,IAAI,EAAE,EACtB,MAAM,CAAC,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,GAC5B,eAAe,CAiDjB"}
1
+ {"version":3,"file":"reach.d.ts","sourceRoot":"","sources":["../../src/expansion/reach.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,aAAa,EAAU,MAAM,UAAU,CAAC;AAC1E,OAAO,KAAK,EACX,IAAI,EACJ,eAAe,EACf,eAAe,EAEf,MAAM,SAAS,CAAC;AAIjB;;;;;;;;;;;GAWG;AACH,wBAAgB,KAAK,CAAC,CAAC,SAAS,QAAQ,EAAE,CAAC,SAAS,QAAQ,EAC3D,KAAK,EAAE,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,EAC1B,KAAK,EAAE,SAAS,IAAI,EAAE,EACtB,MAAM,CAAC,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,GAC5B,eAAe,CA2EjB"}
@@ -1 +1 @@
1
- {"version":3,"file":"sage.d.ts","sourceRoot":"","sources":["../../src/expansion/sage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,aAAa,EAAU,MAAM,UAAU,CAAC;AAC1E,OAAO,KAAK,EACX,IAAI,EACJ,eAAe,EACf,eAAe,EAEf,MAAM,SAAS,CAAC;AAGjB;;;;;;;;;;;GAWG;AACH,wBAAgB,IAAI,CAAC,CAAC,SAAS,QAAQ,EAAE,CAAC,SAAS,QAAQ,EAC1D,KAAK,EAAE,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,EAC1B,KAAK,EAAE,SAAS,IAAI,EAAE,EACtB,MAAM,CAAC,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,GAC5B,eAAe,CAsDjB"}
1
+ {"version":3,"file":"sage.d.ts","sourceRoot":"","sources":["../../src/expansion/sage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,aAAa,EAAU,MAAM,UAAU,CAAC;AAC1E,OAAO,KAAK,EACX,IAAI,EACJ,eAAe,EACf,eAAe,EAEf,MAAM,SAAS,CAAC;AAGjB;;;;;;;;;;;GAWG;AACH,wBAAgB,IAAI,CAAC,CAAC,SAAS,QAAQ,EAAE,CAAC,SAAS,QAAQ,EAC1D,KAAK,EAAE,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,EAC1B,KAAK,EAAE,SAAS,IAAI,EAAE,EACtB,MAAM,CAAC,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,GAC5B,eAAe,CAiDjB"}
@@ -26,6 +26,8 @@ function base(graph, seeds, config) {
26
26
  const { maxNodes = 0, maxIterations = 0, maxPaths = 0, priority = degreePriority, debug = false } = config ?? {};
27
27
  if (seeds.length === 0) return emptyResult("base", startTime);
28
28
  const numFrontiers = seeds.length;
29
+ const allVisited = /* @__PURE__ */ new Set();
30
+ const combinedVisited = /* @__PURE__ */ new Map();
29
31
  const visitedByFrontier = [];
30
32
  const predecessors = [];
31
33
  const queues = [];
@@ -37,15 +39,16 @@ function base(graph, seeds, config) {
37
39
  if (seed === void 0) continue;
38
40
  const seedNode = seed.id;
39
41
  predecessors[i]?.set(seedNode, null);
40
- const seedPriority = priority(seedNode, createPriorityContext(graph, seedNode, i, visitedByFrontier, [], 0));
42
+ combinedVisited.set(seedNode, i);
43
+ allVisited.add(seedNode);
44
+ const seedPriority = priority(seedNode, createPriorityContext(graph, seedNode, i, combinedVisited, allVisited, [], 0));
41
45
  queues[i]?.push({
42
46
  nodeId: seedNode,
43
47
  frontierIndex: i,
44
48
  predecessor: null
45
49
  }, seedPriority);
46
50
  }
47
- const allVisited = /* @__PURE__ */ new Set();
48
- const sampledEdges = /* @__PURE__ */ new Set();
51
+ const sampledEdgeMap = /* @__PURE__ */ new Map();
49
52
  const discoveredPaths = [];
50
53
  let iterations = 0;
51
54
  let edgesTraversed = 0;
@@ -90,6 +93,7 @@ function base(graph, seeds, config) {
90
93
  const frontierVisited = visitedByFrontier[activeFrontier];
91
94
  if (frontierVisited === void 0 || frontierVisited.has(nodeId)) continue;
92
95
  frontierVisited.set(nodeId, activeFrontier);
96
+ combinedVisited.set(nodeId, activeFrontier);
93
97
  if (predecessor !== null) {
94
98
  const predMap = predecessors[activeFrontier];
95
99
  if (predMap !== void 0) predMap.set(nodeId, predecessor);
@@ -111,11 +115,16 @@ function base(graph, seeds, config) {
111
115
  const neighbours = graph.neighbours(nodeId);
112
116
  for (const neighbour of neighbours) {
113
117
  edgesTraversed++;
114
- const edgeKey = nodeId < neighbour ? `${nodeId}::${neighbour}` : `${neighbour}::${nodeId}`;
115
- sampledEdges.add(edgeKey);
118
+ const [s, t] = nodeId < neighbour ? [nodeId, neighbour] : [neighbour, nodeId];
119
+ let targets = sampledEdgeMap.get(s);
120
+ if (targets === void 0) {
121
+ targets = /* @__PURE__ */ new Set();
122
+ sampledEdgeMap.set(s, targets);
123
+ }
124
+ targets.add(t);
116
125
  const frontierVisited = visitedByFrontier[activeFrontier];
117
126
  if (frontierVisited === void 0 || frontierVisited.has(neighbour)) continue;
118
- const neighbourPriority = priority(neighbour, createPriorityContext(graph, neighbour, activeFrontier, visitedByFrontier, discoveredPaths, iterations + 1));
127
+ const neighbourPriority = priority(neighbour, createPriorityContext(graph, neighbour, activeFrontier, combinedVisited, allVisited, discoveredPaths, iterations + 1));
119
128
  queue.push({
120
129
  nodeId: neighbour,
121
130
  frontierIndex: activeFrontier,
@@ -127,14 +136,7 @@ function base(graph, seeds, config) {
127
136
  const endTime = performance.now();
128
137
  const visitedPerFrontier = visitedByFrontier.map((m) => new Set(m.keys()));
129
138
  const edgeTuples = /* @__PURE__ */ new Set();
130
- for (const edgeKey of sampledEdges) {
131
- const parts = edgeKey.split("::");
132
- if (parts.length === 2) {
133
- const source = parts[0];
134
- const target = parts[1];
135
- if (source !== void 0 && target !== void 0) edgeTuples.add([source, target]);
136
- }
137
- }
139
+ for (const [source, targets] of sampledEdgeMap) for (const target of targets) edgeTuples.add([source, target]);
138
140
  return {
139
141
  paths: discoveredPaths,
140
142
  sampledNodes: allVisited,
@@ -154,10 +156,7 @@ function base(graph, seeds, config) {
154
156
  /**
155
157
  * Create priority context for a node.
156
158
  */
157
- function createPriorityContext(graph, nodeId, frontierIndex, visitedByFrontier, discoveredPaths, iteration) {
158
- const combinedVisited = /* @__PURE__ */ new Map();
159
- for (const frontierMap of visitedByFrontier) for (const [id, idx] of frontierMap) combinedVisited.set(id, idx);
160
- const allVisited = new Set(combinedVisited.keys());
159
+ function createPriorityContext(graph, nodeId, frontierIndex, combinedVisited, allVisited, discoveredPaths, iteration) {
161
160
  return {
162
161
  graph,
163
162
  degree: graph.degree(nodeId),
@@ -341,10 +340,12 @@ function hae(graph, seeds, config) {
341
340
  * visited by OTHER frontiers (not the current frontier).
342
341
  */
343
342
  function pipePriority(nodeId, context) {
344
- const graph = context.graph;
345
- const neighbours = new Set(graph.neighbours(nodeId));
343
+ const neighbours = context.graph.neighbours(nodeId);
346
344
  let pathPotential = 0;
347
- for (const [visitedId, frontierIdx] of context.visitedByFrontier) if (frontierIdx !== context.frontierIndex && neighbours.has(visitedId)) pathPotential++;
345
+ for (const neighbour of neighbours) {
346
+ const visitedBy = context.visitedByFrontier.get(neighbour);
347
+ if (visitedBy !== void 0 && visitedBy !== context.frontierIndex) pathPotential++;
348
+ }
348
349
  return context.degree / (1 + pathPotential);
349
350
  }
350
351
  /**
@@ -387,10 +388,7 @@ function sage(graph, seeds, config) {
387
388
  */
388
389
  function sagePriority(nodeId, context) {
389
390
  const pathCount = context.discoveredPaths.length;
390
- if (pathCount > 0 && !inPhase2) {
391
- inPhase2 = true;
392
- for (const path of context.discoveredPaths) for (const node of path.nodes) salienceCounts.set(node, (salienceCounts.get(node) ?? 0) + 1);
393
- }
391
+ if (pathCount > 0 && !inPhase2) inPhase2 = true;
394
392
  if (pathCount > lastPathCount) {
395
393
  for (let i = lastPathCount; i < pathCount; i++) {
396
394
  const path = context.discoveredPaths[i];
@@ -440,6 +438,23 @@ function jaccard(graph, source, target, config) {
440
438
  */
441
439
  function reach(graph, seeds, config) {
442
440
  let inPhase2 = false;
441
+ const jaccardCache = /* @__PURE__ */ new Map();
442
+ /**
443
+ * Compute Jaccard similarity with caching.
444
+ *
445
+ * Exploits symmetry of Jaccard (J(A,B) = J(B,A)) to reduce
446
+ * duplicate computations when the same pair appears in multiple
447
+ * discovered paths. Key format ensures consistent ordering.
448
+ */
449
+ function cachedJaccard(source, target) {
450
+ const key = source < target ? `${source}::${target}` : `${target}::${source}`;
451
+ let score = jaccardCache.get(key);
452
+ if (score === void 0) {
453
+ score = jaccard(graph, source, target);
454
+ jaccardCache.set(key, score);
455
+ }
456
+ return score;
457
+ }
443
458
  /**
444
459
  * REACH priority function with MI estimation.
445
460
  */
@@ -451,8 +466,8 @@ function reach(graph, seeds, config) {
451
466
  for (const path of context.discoveredPaths) {
452
467
  const fromNodeId = path.fromSeed.id;
453
468
  const toNodeId = path.toSeed.id;
454
- totalMI += jaccard(graph, nodeId, fromNodeId);
455
- totalMI += jaccard(graph, nodeId, toNodeId);
469
+ totalMI += cachedJaccard(nodeId, fromNodeId);
470
+ totalMI += cachedJaccard(nodeId, toNodeId);
456
471
  endpointCount += 2;
457
472
  }
458
473
  const miHat = endpointCount > 0 ? totalMI / endpointCount : 0;
@@ -500,9 +515,12 @@ function maze(graph, seeds, config) {
500
515
  }
501
516
  lastPathCount = pathCount;
502
517
  }
503
- const nodeNeighbours = new Set(graph.neighbours(nodeId));
518
+ const nodeNeighbours = graph.neighbours(nodeId);
504
519
  let pathPotential = 0;
505
- for (const [visitedId, frontierIdx] of context.visitedByFrontier) if (frontierIdx !== context.frontierIndex && nodeNeighbours.has(visitedId)) pathPotential++;
520
+ for (const neighbour of nodeNeighbours) {
521
+ const visitedBy = context.visitedByFrontier.get(neighbour);
522
+ if (visitedBy !== void 0 && visitedBy !== context.frontierIndex) pathPotential++;
523
+ }
506
524
  if (!inPhase2) return context.degree / (1 + pathPotential);
507
525
  const salience = salienceCounts.get(nodeId) ?? 0;
508
526
  return context.degree / (1 + pathPotential) * (1 / (1 + SALIENCE_WEIGHT * salience));