66 */
77
88import { DegreePrioritisedExpansion } from "@graph/algorithms/traversal/degree-prioritised-expansion.js" ;
9+ import { EntropyGuidedExpansion } from "@graph/algorithms/traversal/entropy-guided-expansion.js" ;
10+ import { IntelligentDelayedTermination } from "@graph/algorithms/traversal/intelligent-delayed-termination.js" ;
11+ import { PathPreservingExpansion } from "@graph/algorithms/traversal/path-preserving-expansion.js" ;
12+ import { RetrospectiveSalienceExpansion } from "@graph/algorithms/traversal/retrospective-salience-expansion.js" ;
913import { BenchmarkGraphExpander } from "@graph/evaluation/__tests__/validation/common/benchmark-graph-expander.js" ;
1014import { loadBenchmarkByIdFromUrl } from "@graph/evaluation/fixtures/index.js" ;
15+ import { CrossSeedAffinityExpansion } from "@graph/experiments/baselines/cross-seed-affinity.js" ;
16+ import { DegreeSurpriseExpansion } from "@graph/experiments/baselines/degree-surprise.js" ;
17+ import { DelayedTerminationExpansion } from "@graph/experiments/baselines/delayed-termination.js" ;
18+ import { EnsembleExpansion } from "@graph/experiments/baselines/ensemble-expansion.js" ;
1119import { FrontierBalancedExpansion } from "@graph/experiments/baselines/frontier-balanced.js" ;
1220import { RandomPriorityExpansion } from "@graph/experiments/baselines/random-priority.js" ;
1321import { retroactivePathEnumeration } from "@graph/experiments/baselines/retroactive-path-enum.js" ;
1422import { StandardBfsExpansion } from "@graph/experiments/baselines/standard-bfs.js" ;
1523import { metrics } from "@graph/experiments/metrics/index.js" ;
1624
25+ /**
26+ * Create all expansion methods for a given expander and seeds.
27+ * Matches the method list from salience-coverage-comparison.ts for consistency.
28+ * @param expander - Graph expander providing neighbour access
29+ * @param seeds - Array of seed node IDs
30+ */
31+ const createAllMethods = ( expander : BenchmarkGraphExpander , seeds : readonly string [ ] ) => [
32+ // Baselines
33+ { name : "Standard BFS" , create : ( ) => new StandardBfsExpansion ( expander , seeds ) } ,
34+ { name : "Degree-Prioritised" , create : ( ) => new DegreePrioritisedExpansion ( expander , seeds ) } ,
35+ { name : "Frontier-Balanced" , create : ( ) => new FrontierBalancedExpansion ( expander , seeds ) } ,
36+ { name : "Random Priority" , create : ( ) => new RandomPriorityExpansion ( expander , seeds , 42 ) } ,
37+ // Novel algorithms
38+ { name : "Entropy-Guided (EGE)" , create : ( ) => new EntropyGuidedExpansion ( expander , seeds ) } ,
39+ { name : "Path-Preserving (PPME)" , create : ( ) => new PathPreservingExpansion ( expander , seeds ) } ,
40+ { name : "Retrospective Salience (RSGE)" , create : ( ) => new RetrospectiveSalienceExpansion ( expander , seeds ) } ,
41+ // Baseline variants
42+ { name : "Delayed Termination +50" , create : ( ) => new DelayedTerminationExpansion ( expander , seeds , { delayIterations : 50 } ) } ,
43+ { name : "Delayed Termination +100" , create : ( ) => new DelayedTerminationExpansion ( expander , seeds , { delayIterations : 100 } ) } ,
44+ { name : "Degree Surprise" , create : ( ) => new DegreeSurpriseExpansion ( expander , seeds ) } ,
45+ { name : "Ensemble (BFS∪DFS∪DP)" , create : ( ) => new EnsembleExpansion ( expander , seeds ) } ,
46+ { name : "Cross-Seed Affinity" , create : ( ) => new CrossSeedAffinityExpansion ( expander , seeds ) } ,
47+ // Intelligent termination strategies
48+ { name : "Intelligent Delayed +50" , create : ( ) => new IntelligentDelayedTermination ( expander , seeds , { delayIterations : 50 } ) } ,
49+ { name : "Intelligent Delayed +100" , create : ( ) => new IntelligentDelayedTermination ( expander , seeds , { delayIterations : 100 } ) } ,
50+ ] ;
51+
52+
1753const DATASET_NAMES : Record < string , string > = {
1854 "karate" : "Karate Club" ,
1955 "lesmis" : "Les Misérables" ,
@@ -45,15 +81,10 @@ export const runHubTraversalExperiments = async (): Promise<void> => {
4581 const seeds : [ string , string ] = [ allNodes [ 0 ] , allNodes . at ( - 1 ) ?? allNodes [ 0 ] ] ;
4682
4783 // Run each method
48- const methods = [
49- { name : "Degree-Prioritised" , algo : new DegreePrioritisedExpansion ( expander , seeds ) } ,
50- { name : "Standard BFS" , algo : new StandardBfsExpansion ( expander , seeds ) } ,
51- { name : "Frontier-Balanced" , algo : new FrontierBalancedExpansion ( expander , seeds ) } ,
52- { name : "Random Priority" , algo : new RandomPriorityExpansion ( expander , seeds , 42 ) } ,
53- ] ;
84+ const methods = createAllMethods ( expander , seeds ) ;
5485
5586 for ( const method of methods ) {
56- const result = await method . algo . run ( ) ;
87+ const result = await method . create ( ) . run ( ) ;
5788
5889 // Calculate hub traversal: % of nodes with degree > threshold that were visited
5990 const threshold = 5 ;
@@ -97,37 +128,50 @@ export const runRuntimeExperiments = async (): Promise<void> => {
97128 const allNodes = expander . getAllNodeIds ( ) ;
98129 const seeds : [ string , string ] = [ allNodes [ 0 ] , allNodes . at ( - 1 ) ?? allNodes [ 0 ] ] ;
99130
100- // Time DP
101- const dpStart = performance . now ( ) ;
102- const dp = new DegreePrioritisedExpansion ( expander , seeds ) ;
103- const dpResult = await dp . run ( ) ;
104- const dpTime = performance . now ( ) - dpStart ;
105-
106- // Time BFS
107- const bfsStart = performance . now ( ) ;
108- const bfs = new StandardBfsExpansion ( expander , seeds ) ;
109- const bfsResult = await bfs . run ( ) ;
110- const bfsTime = performance . now ( ) - bfsStart ;
111-
112- const dpNodesPerSec = Math . round ( dpResult . sampledNodes . size / ( dpTime / 1000 ) ) ;
113- const bfsNodesPerSec = Math . round ( bfsResult . sampledNodes . size / ( bfsTime / 1000 ) ) ;
114-
115- metrics . record ( "runtime-performance" , {
116- dataset : DATASET_NAMES [ id ] ?? id ,
117- nodes : expectedNodes ,
118- dpTime : Math . round ( dpTime * 100 ) / 100 ,
119- bfsTime : Math . round ( bfsTime * 100 ) / 100 ,
120- dpNodesPerSec,
121- bfsNodesPerSec,
122- } ) ;
131+ const allMethods = createAllMethods ( expander , seeds ) ;
123132
124- metrics . record ( "scalability" , {
125- dataset : id ,
126- nodes : expectedNodes ,
127- dpTime : Math . round ( dpTime * 10 ) / 10 ,
128- bfsTime : Math . round ( bfsTime * 10 ) / 10 ,
129- ratio : Math . round ( ( bfsTime / dpTime ) * 100 ) / 100 ,
130- } ) ;
133+ // Collect timing data for all methods
134+ const timings : Array < { name : string ; timeMs : number ; nodesExpanded : number } > = [ ] ;
135+
136+ for ( const method of allMethods ) {
137+ const start = performance . now ( ) ;
138+ const result = await method . create ( ) . run ( ) ;
139+ const elapsed = performance . now ( ) - start ;
140+ const nodesExpanded = result . sampledNodes . size ;
141+
142+ timings . push ( { name : method . name , timeMs : elapsed , nodesExpanded } ) ;
143+
144+ metrics . record ( "runtime-performance-all" , {
145+ dataset : DATASET_NAMES [ id ] ?? id ,
146+ nodes : expectedNodes ,
147+ method : method . name ,
148+ timeMs : Math . round ( elapsed * 100 ) / 100 ,
149+ nodesPerSec : Math . round ( nodesExpanded / ( elapsed / 1000 ) ) ,
150+ } ) ;
151+ }
152+
153+ // Preserve legacy DP-vs-BFS metrics for backward compatibility
154+ const dpTiming = timings . find ( ( t ) => t . name === "Degree-Prioritised" ) ;
155+ const bfsTiming = timings . find ( ( t ) => t . name === "Standard BFS" ) ;
156+
157+ if ( dpTiming && bfsTiming ) {
158+ metrics . record ( "runtime-performance" , {
159+ dataset : DATASET_NAMES [ id ] ?? id ,
160+ nodes : expectedNodes ,
161+ dpTime : Math . round ( dpTiming . timeMs * 100 ) / 100 ,
162+ bfsTime : Math . round ( bfsTiming . timeMs * 100 ) / 100 ,
163+ dpNodesPerSec : Math . round ( dpTiming . nodesExpanded / ( dpTiming . timeMs / 1000 ) ) ,
164+ bfsNodesPerSec : Math . round ( bfsTiming . nodesExpanded / ( bfsTiming . timeMs / 1000 ) ) ,
165+ } ) ;
166+
167+ metrics . record ( "scalability" , {
168+ dataset : id ,
169+ nodes : expectedNodes ,
170+ dpTime : Math . round ( dpTiming . timeMs * 10 ) / 10 ,
171+ bfsTime : Math . round ( bfsTiming . timeMs * 10 ) / 10 ,
172+ ratio : Math . round ( ( bfsTiming . timeMs / dpTiming . timeMs ) * 100 ) / 100 ,
173+ } ) ;
174+ }
131175 }
132176} ;
133177
@@ -143,55 +187,31 @@ export const runPathDiversityExperiments = async (): Promise<void> => {
143187 const allNodes = expander . getAllNodeIds ( ) ;
144188 const seeds : [ string , string ] = [ allNodes [ 0 ] , allNodes . at ( - 1 ) ?? allNodes [ 0 ] ] ;
145189
146- // Run DP
147- const dp = new DegreePrioritisedExpansion ( expander , seeds ) ;
148- const dpResult = await dp . run ( ) ;
149-
150- // Use retroactive path enumeration for fair comparison (maxLength=5 for tractability)
151- const dpRetroPaths = await retroactivePathEnumeration ( dpResult , expander , seeds , 5 ) ;
152-
153- // Calculate path length distribution from retroactive paths
154- const pathLengths = dpRetroPaths . paths . map ( ( p ) => p . nodes . length ) ;
155- if ( pathLengths . length > 0 ) {
156- const minLength = Math . min ( ...pathLengths ) ;
157- const maxLength = Math . max ( ...pathLengths ) ;
158- const meanLength = pathLengths . reduce ( ( a , b ) => a + b , 0 ) / pathLengths . length ;
159- const sortedLengths = [ ...pathLengths ] . sort ( ( a , b ) => a - b ) ;
160- const medianLength = sortedLengths [ Math . floor ( sortedLengths . length / 2 ) ] ;
161-
162- metrics . record ( "path-lengths" , {
163- dataset : "Les Misérables" ,
164- method : "Degree-Prioritised" ,
165- min : minLength ,
166- max : maxLength ,
167- mean : Math . round ( meanLength * 100 ) / 100 ,
168- median : medianLength ,
169- } ) ;
170- }
190+ const allMethods = createAllMethods ( expander , seeds ) ;
171191
172- // Run BFS for comparison
173- const bfs = new StandardBfsExpansion ( expander , seeds ) ;
174- const bfsResult = await bfs . run ( ) ;
175-
176- // Use retroactive path enumeration for fair comparison (maxLength=5 for tractability)
177- const bfsRetroPaths = await retroactivePathEnumeration ( bfsResult , expander , seeds , 5 ) ;
178-
179- const bfsPathLengths = bfsRetroPaths . paths . map ( ( p ) => p . nodes . length ) ;
180- if ( bfsPathLengths . length > 0 ) {
181- const bfsMinLength = Math . min ( ...bfsPathLengths ) ;
182- const bsfMaxLength = Math . max ( ... bfsPathLengths ) ;
183- const bfsMeanLength = bfsPathLengths . reduce ( ( a , b ) => a + b , 0 ) / bfsPathLengths . length ;
184- const bfsSortedLengths = [ ... bfsPathLengths ] . sort ( ( a , b ) => a - b ) ;
185- const bfsMedianLength = bfsSortedLengths [ Math . floor ( bfsSortedLengths . length / 2 ) ] ;
186-
187- metrics . record ( "path-lengths" , {
188- dataset : "Les Misérables" ,
189- method : "Standard BFS" ,
190- min : bfsMinLength ,
191- max : bsfMaxLength ,
192- mean : Math . round ( bfsMeanLength * 100 ) / 100 ,
193- median : bfsMedianLength ,
194- } ) ;
192+ for ( const method of allMethods ) {
193+ const result = await method . create ( ) . run ( ) ;
194+
195+ // Use retroactive path enumeration for fair comparison (maxLength=5 for tractability)
196+ const retroPaths = await retroactivePathEnumeration ( result , expander , seeds , 5 ) ;
197+
198+ const pathLengths = retroPaths . paths . map ( ( p ) => p . nodes . length ) ;
199+ if ( pathLengths . length > 0 ) {
200+ const minLength = Math . min ( ... pathLengths ) ;
201+ const maxLength = Math . max ( ...pathLengths ) ;
202+ const meanLength = pathLengths . reduce ( ( a , b ) => a + b , 0 ) / pathLengths . length ;
203+ const sortedLengths = [ ... pathLengths ] . sort ( ( a , b ) => a - b ) ;
204+ const medianLength = sortedLengths [ Math . floor ( sortedLengths . length / 2 ) ] ;
205+
206+ metrics . record ( "path-lengths" , {
207+ dataset : "Les Misérables" ,
208+ method : method . name ,
209+ min : minLength ,
210+ max : maxLength ,
211+ mean : Math . round ( meanLength * 100 ) / 100 ,
212+ median : medianLength ,
213+ } ) ;
214+ }
195215 }
196216} ;
197217
@@ -223,17 +243,12 @@ export const runMethodRankingExperiments = async (): Promise<void> => {
223243 return totalNodes > 0 ? allNodes . size / totalNodes : 0 ;
224244 } ;
225245
226- const methods = [
227- { name : "Degree-Prioritised (Thesis)" , algo : new DegreePrioritisedExpansion ( expander , seeds ) } ,
228- { name : "Random Priority" , algo : new RandomPriorityExpansion ( expander , seeds , 42 ) } ,
229- { name : "Standard BFS" , algo : new StandardBfsExpansion ( expander , seeds ) } ,
230- { name : "Frontier-Balanced" , algo : new FrontierBalancedExpansion ( expander , seeds ) } ,
231- ] ;
246+ const allMethods = createAllMethods ( expander , seeds ) ;
232247
233248 const rankings : Array < { method : string ; diversity : number ; paths : number ; onlinePaths : number } > = [ ] ;
234249
235- for ( const method of methods ) {
236- const result = await method . algo . run ( ) ;
250+ for ( const method of allMethods ) {
251+ const result = await method . create ( ) . run ( ) ;
237252
238253 // Use retroactive path enumeration for fair comparison (maxLength=5 for tractability)
239254 const retroactivePaths = await retroactivePathEnumeration ( result , expander , seeds , 5 ) ;
@@ -296,19 +311,20 @@ export const runCrossDatasetExperiments = async (): Promise<void> => {
296311 const allNodes = expander . getAllNodeIds ( ) ;
297312 const seeds : [ string , string ] = [ allNodes [ 0 ] , allNodes . at ( - 1 ) ?? allNodes [ 0 ] ] ;
298313
299- const methods = [
300- { name : "Degree-Prioritised" , algo : new DegreePrioritisedExpansion ( expander , seeds ) } ,
301- { name : "Standard BFS" , algo : new StandardBfsExpansion ( expander , seeds ) } ,
302- { name : "Frontier-Balanced" , algo : new FrontierBalancedExpansion ( expander , seeds ) } ,
303- { name : "Random Priority" , algo : new RandomPriorityExpansion ( expander , seeds , 42 ) } ,
304- ] ;
314+ const allMethods = createAllMethods ( expander , seeds ) ;
305315
306- for ( const method of methods ) {
307- const result = await method . algo . run ( ) ;
316+ for ( const method of allMethods ) {
317+ const result = await method . create ( ) . run ( ) ;
308318 const retroPaths = await retroactivePathEnumeration ( result , expander , seeds , 5 ) ;
309319 const retroDiversity = calculateDiversity ( retroPaths . paths ) ;
310320 const onlineDiversity = calculateDiversity ( result . paths ) ;
311321
322+ // Extract nodesExpanded from stats, handling different result types
323+ const nodesExpanded =
324+ "nodesExpanded" in result . stats
325+ ? ( result . stats as { nodesExpanded : number } ) . nodesExpanded
326+ : result . sampledNodes . size ;
327+
312328 metrics . record ( "cross-dataset" , {
313329 dataset : dataset . name ,
314330 nodes : dataset . nodes ,
@@ -317,7 +333,7 @@ export const runCrossDatasetExperiments = async (): Promise<void> => {
317333 retroactivePaths : retroPaths . paths . length ,
318334 onlineDiversity : Math . round ( onlineDiversity * 1000 ) / 1000 ,
319335 retroactiveDiversity : Math . round ( retroDiversity * 1000 ) / 1000 ,
320- nodesExpanded : result . stats . nodesExpanded ,
336+ nodesExpanded,
321337 } ) ;
322338 }
323339 }
0 commit comments