Skip to content

Commit 2a2d7c4

Browse files
committed
feat(traversal): add node discovery iteration tracking to result interfaces
track when each node is first discovered during expansion to enable coverage efficiency metrics. maps node id to iteration number.
1 parent d39d9dc commit 2a2d7c4

2 files changed

Lines changed: 42 additions & 3 deletions

File tree

src/algorithms/traversal/degree-prioritised-expansion.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ export interface DegreePrioritisedExpansionResult {
1919

2020
/** Statistics about the expansion */
2121
stats: ExpansionStats;
22+
23+
/**
24+
* Maps each sampled node to the iteration when it was first discovered.
25+
* Used for computing coverage efficiency metrics (first-discovery iteration,
26+
* budget checkpoint coverage, area-under-curve).
27+
*/
28+
nodeDiscoveryIteration: Map<string, number>;
2229
}
2330

2431
/**
@@ -118,6 +125,8 @@ export class DegreePrioritisedExpansion<T> {
118125
private readonly paths: Array<{ fromSeed: number; toSeed: number; nodes: string[] }> = [];
119126
private readonly sampledEdges = new Set<string>();
120127
private stats: ExpansionStats;
128+
/** Tracks when each node was first discovered (iteration number) */
129+
private readonly nodeDiscoveryIteration = new Map<string, number>();
121130

122131
/** Track which frontier owns each node for O(1) intersection checking */
123132
private readonly nodeToFrontierIndex = new Map<string, number>();
@@ -155,6 +164,9 @@ export class DegreePrioritisedExpansion<T> {
155164

156165
// Track which frontier owns this seed
157166
this.nodeToFrontierIndex.set(seed, index);
167+
168+
// Seeds are discovered at iteration 0
169+
this.nodeDiscoveryIteration.set(seed, 0);
158170
}
159171

160172
this.stats = {
@@ -208,6 +220,11 @@ export class DegreePrioritisedExpansion<T> {
208220
activeState.visited.add(targetId);
209221
activeState.parents.set(targetId, { parent: node, edge: relationshipType });
210222

223+
// Track first discovery iteration (only if not already discovered by another frontier)
224+
if (!this.nodeDiscoveryIteration.has(targetId)) {
225+
this.nodeDiscoveryIteration.set(targetId, this.stats.iterations);
226+
}
227+
211228
// Check for intersection using O(1) lookup BEFORE claiming ownership
212229
// If another frontier already visited this node, we have a path
213230
const otherFrontierIndex = this.nodeToFrontierIndex.get(targetId);
@@ -255,6 +272,7 @@ export class DegreePrioritisedExpansion<T> {
255272
sampledEdges: this.sampledEdges,
256273
visitedPerFrontier,
257274
stats: this.stats,
275+
nodeDiscoveryIteration: this.nodeDiscoveryIteration,
258276
};
259277
}
260278

@@ -339,12 +357,24 @@ export class DegreePrioritisedExpansion<T> {
339357
if (pathFromB.length > 0 && pathFromB.at(-1) !== seedB && // Path from B should end at seed B, or be empty if meeting node is seed B
340358
meetingNode !== seedB) return null;
341359

342-
return [...pathFromA, ...pathFromB];
360+
const fullPath = [...pathFromA, ...pathFromB];
361+
362+
// Validate simple path (no repeated nodes)
363+
// This can happen when both frontiers explore through the same intermediate
364+
// node before meeting at a further node. In such cases, a shorter path
365+
// connecting the seeds should already exist.
366+
const nodeSet = new Set(fullPath);
367+
if (nodeSet.size !== fullPath.length) {
368+
return null;
369+
}
370+
371+
return fullPath;
343372
}
344373

345374
/**
346375
* Create a unique signature for a path to enable O(1) deduplication.
347-
* Signature is bidirectional (A-B same as B-A).
376+
* Signature is bidirectional (A-B same as B-A) and includes the actual
377+
* node sequence to distinguish different paths with the same length.
348378
* @param fromSeed
349379
* @param toSeed
350380
* @param nodes
@@ -353,7 +383,9 @@ export class DegreePrioritisedExpansion<T> {
353383
private createPathSignature(fromSeed: number, toSeed: number, nodes: string[]): string {
354384
// Sort seed indices to make signature bidirectional
355385
const [a, b] = fromSeed < toSeed ? [fromSeed, toSeed] : [toSeed, fromSeed];
356-
return `${a}-${b}-${nodes.length}`;
386+
// Include actual node sequence, normalized to consistent direction
387+
const normalizedNodes = a === fromSeed ? nodes.join("-") : [...nodes].reverse().join("-");
388+
return `${a}-${b}-${normalizedNodes}`;
357389
}
358390

359391
/**

src/algorithms/traversal/overlap-based/overlap-result.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ export interface OverlapBasedExpansionResult {
2121

2222
/** Overlap-specific metadata */
2323
overlapMetadata: OverlapMetadata;
24+
25+
/**
26+
* Maps each sampled node to the iteration when it was first discovered.
27+
* Used for computing coverage efficiency metrics (first-discovery iteration,
28+
* budget checkpoint coverage, area-under-curve).
29+
*/
30+
nodeDiscoveryIteration: Map<string, number>;
2431
}
2532

2633
/**

0 commit comments

Comments
 (0)