Skip to content

Commit 38900b1

Browse files
committed
feat(experiments): extend all expansion experiments to test all 14 algorithms
Replace hardcoded 4-method lists with shared createAllMethods() and createAllMethodsWithHubThreshold() helpers matching salience-coverage's full method inventory. Hub traversal, runtime, path diversity, cross-dataset, and hub encounter order experiments now cover all 14 expansion algorithms. Add runtime-performance-all metric type for per-method timing data.
1 parent 17003ae commit 38900b1

3 files changed

Lines changed: 217 additions & 133 deletions

File tree

src/experiments/experiments/bidirectional-bfs.ts

Lines changed: 118 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,50 @@
66
*/
77

88
import { 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";
913
import { BenchmarkGraphExpander } from "@graph/evaluation/__tests__/validation/common/benchmark-graph-expander.js";
1014
import { 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";
1119
import { FrontierBalancedExpansion } from "@graph/experiments/baselines/frontier-balanced.js";
1220
import { RandomPriorityExpansion } from "@graph/experiments/baselines/random-priority.js";
1321
import { retroactivePathEnumeration } from "@graph/experiments/baselines/retroactive-path-enum.js";
1422
import { StandardBfsExpansion } from "@graph/experiments/baselines/standard-bfs.js";
1523
import { 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+
1753
const 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

Comments
 (0)