graphwise 1.4.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -25,6 +25,8 @@ function base(graph, seeds, config) {
25
25
  const { maxNodes = 0, maxIterations = 0, maxPaths = 0, priority = degreePriority, debug = false } = config ?? {};
26
26
  if (seeds.length === 0) return emptyResult("base", startTime);
27
27
  const numFrontiers = seeds.length;
28
+ const allVisited = /* @__PURE__ */ new Set();
29
+ const combinedVisited = /* @__PURE__ */ new Map();
28
30
  const visitedByFrontier = [];
29
31
  const predecessors = [];
30
32
  const queues = [];
@@ -36,15 +38,16 @@ function base(graph, seeds, config) {
36
38
  if (seed === void 0) continue;
37
39
  const seedNode = seed.id;
38
40
  predecessors[i]?.set(seedNode, null);
39
- const seedPriority = priority(seedNode, createPriorityContext(graph, seedNode, i, visitedByFrontier, [], 0));
41
+ combinedVisited.set(seedNode, i);
42
+ allVisited.add(seedNode);
43
+ const seedPriority = priority(seedNode, createPriorityContext(graph, seedNode, i, combinedVisited, allVisited, [], 0));
40
44
  queues[i]?.push({
41
45
  nodeId: seedNode,
42
46
  frontierIndex: i,
43
47
  predecessor: null
44
48
  }, seedPriority);
45
49
  }
46
- const allVisited = /* @__PURE__ */ new Set();
47
- const sampledEdges = /* @__PURE__ */ new Set();
50
+ const sampledEdgeMap = /* @__PURE__ */ new Map();
48
51
  const discoveredPaths = [];
49
52
  let iterations = 0;
50
53
  let edgesTraversed = 0;
@@ -89,6 +92,7 @@ function base(graph, seeds, config) {
89
92
  const frontierVisited = visitedByFrontier[activeFrontier];
90
93
  if (frontierVisited === void 0 || frontierVisited.has(nodeId)) continue;
91
94
  frontierVisited.set(nodeId, activeFrontier);
95
+ combinedVisited.set(nodeId, activeFrontier);
92
96
  if (predecessor !== null) {
93
97
  const predMap = predecessors[activeFrontier];
94
98
  if (predMap !== void 0) predMap.set(nodeId, predecessor);
@@ -110,11 +114,16 @@ function base(graph, seeds, config) {
110
114
  const neighbours = graph.neighbours(nodeId);
111
115
  for (const neighbour of neighbours) {
112
116
  edgesTraversed++;
113
- const edgeKey = nodeId < neighbour ? `${nodeId}::${neighbour}` : `${neighbour}::${nodeId}`;
114
- sampledEdges.add(edgeKey);
117
+ const [s, t] = nodeId < neighbour ? [nodeId, neighbour] : [neighbour, nodeId];
118
+ let targets = sampledEdgeMap.get(s);
119
+ if (targets === void 0) {
120
+ targets = /* @__PURE__ */ new Set();
121
+ sampledEdgeMap.set(s, targets);
122
+ }
123
+ targets.add(t);
115
124
  const frontierVisited = visitedByFrontier[activeFrontier];
116
125
  if (frontierVisited === void 0 || frontierVisited.has(neighbour)) continue;
117
- const neighbourPriority = priority(neighbour, createPriorityContext(graph, neighbour, activeFrontier, visitedByFrontier, discoveredPaths, iterations + 1));
126
+ const neighbourPriority = priority(neighbour, createPriorityContext(graph, neighbour, activeFrontier, combinedVisited, allVisited, discoveredPaths, iterations + 1));
118
127
  queue.push({
119
128
  nodeId: neighbour,
120
129
  frontierIndex: activeFrontier,
@@ -126,14 +135,7 @@ function base(graph, seeds, config) {
126
135
  const endTime = performance.now();
127
136
  const visitedPerFrontier = visitedByFrontier.map((m) => new Set(m.keys()));
128
137
  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
- }
138
+ for (const [source, targets] of sampledEdgeMap) for (const target of targets) edgeTuples.add([source, target]);
137
139
  return {
138
140
  paths: discoveredPaths,
139
141
  sampledNodes: allVisited,
@@ -153,10 +155,7 @@ function base(graph, seeds, config) {
153
155
  /**
154
156
  * Create priority context for a node.
155
157
  */
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());
158
+ function createPriorityContext(graph, nodeId, frontierIndex, combinedVisited, allVisited, discoveredPaths, iteration) {
160
159
  return {
161
160
  graph,
162
161
  degree: graph.degree(nodeId),
@@ -340,10 +339,12 @@ function hae(graph, seeds, config) {
340
339
  * visited by OTHER frontiers (not the current frontier).
341
340
  */
342
341
  function pipePriority(nodeId, context) {
343
- const graph = context.graph;
344
- const neighbours = new Set(graph.neighbours(nodeId));
342
+ const neighbours = context.graph.neighbours(nodeId);
345
343
  let pathPotential = 0;
346
- for (const [visitedId, frontierIdx] of context.visitedByFrontier) if (frontierIdx !== context.frontierIndex && neighbours.has(visitedId)) pathPotential++;
344
+ for (const neighbour of neighbours) {
345
+ const visitedBy = context.visitedByFrontier.get(neighbour);
346
+ if (visitedBy !== void 0 && visitedBy !== context.frontierIndex) pathPotential++;
347
+ }
347
348
  return context.degree / (1 + pathPotential);
348
349
  }
349
350
  /**
@@ -386,10 +387,7 @@ function sage(graph, seeds, config) {
386
387
  */
387
388
  function sagePriority(nodeId, context) {
388
389
  const pathCount = context.discoveredPaths.length;
389
- if (pathCount > 0 && !inPhase2) {
390
- inPhase2 = true;
391
- for (const path of context.discoveredPaths) for (const node of path.nodes) salienceCounts.set(node, (salienceCounts.get(node) ?? 0) + 1);
392
- }
390
+ if (pathCount > 0 && !inPhase2) inPhase2 = true;
393
391
  if (pathCount > lastPathCount) {
394
392
  for (let i = lastPathCount; i < pathCount; i++) {
395
393
  const path = context.discoveredPaths[i];
@@ -439,6 +437,23 @@ function jaccard(graph, source, target, config) {
439
437
  */
440
438
  function reach(graph, seeds, config) {
441
439
  let inPhase2 = false;
440
+ const jaccardCache = /* @__PURE__ */ new Map();
441
+ /**
442
+ * Compute Jaccard similarity with caching.
443
+ *
444
+ * Exploits symmetry of Jaccard (J(A,B) = J(B,A)) to reduce
445
+ * duplicate computations when the same pair appears in multiple
446
+ * discovered paths. Key format ensures consistent ordering.
447
+ */
448
+ function cachedJaccard(source, target) {
449
+ const key = source < target ? `${source}::${target}` : `${target}::${source}`;
450
+ let score = jaccardCache.get(key);
451
+ if (score === void 0) {
452
+ score = jaccard(graph, source, target);
453
+ jaccardCache.set(key, score);
454
+ }
455
+ return score;
456
+ }
442
457
  /**
443
458
  * REACH priority function with MI estimation.
444
459
  */
@@ -450,8 +465,8 @@ function reach(graph, seeds, config) {
450
465
  for (const path of context.discoveredPaths) {
451
466
  const fromNodeId = path.fromSeed.id;
452
467
  const toNodeId = path.toSeed.id;
453
- totalMI += jaccard(graph, nodeId, fromNodeId);
454
- totalMI += jaccard(graph, nodeId, toNodeId);
468
+ totalMI += cachedJaccard(nodeId, fromNodeId);
469
+ totalMI += cachedJaccard(nodeId, toNodeId);
455
470
  endpointCount += 2;
456
471
  }
457
472
  const miHat = endpointCount > 0 ? totalMI / endpointCount : 0;
@@ -499,9 +514,12 @@ function maze(graph, seeds, config) {
499
514
  }
500
515
  lastPathCount = pathCount;
501
516
  }
502
- const nodeNeighbours = new Set(graph.neighbours(nodeId));
517
+ const nodeNeighbours = graph.neighbours(nodeId);
503
518
  let pathPotential = 0;
504
- for (const [visitedId, frontierIdx] of context.visitedByFrontier) if (frontierIdx !== context.frontierIndex && nodeNeighbours.has(visitedId)) pathPotential++;
519
+ for (const neighbour of nodeNeighbours) {
520
+ const visitedBy = context.visitedByFrontier.get(neighbour);
521
+ if (visitedBy !== void 0 && visitedBy !== context.frontierIndex) pathPotential++;
522
+ }
505
523
  if (!inPhase2) return context.degree / (1 + pathPotential);
506
524
  const salience = salienceCounts.get(nodeId) ?? 0;
507
525
  return context.degree / (1 + pathPotential) * (1 / (1 + SALIENCE_WEIGHT * salience));