Skip to content

Commit 4af26d2

Browse files
committed
feat(tests): add n-seed comparison test with console metrics
Add comprehensive n-seed variant comparison across all expansion methods. Log n-seed comparison table, hub traversal rates, and path diversity metrics for N=1, N=2, and N>=3 variants.
1 parent 689f0ae commit 4af26d2

File tree

1 file changed

+314
-0
lines changed

1 file changed

+314
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
/**
2+
* VALIDITY: N-Seed Comparison Across Methods
3+
*
4+
* Compares Degree-Prioritised, Standard BFS, Frontier-Balanced, and Random
5+
* across N=1 (ego-graph), N=2 (between-graph), and N>=3 (multi-seed) variants.
6+
*/
7+
8+
import { describe, expect, it } from "vitest";
9+
10+
import { DegreePrioritisedExpansion } from "../../../../../../../../algorithms/traversal/degree-prioritised-expansion";
11+
import { FrontierBalancedExpansion } from "../../../../../../../baselines/frontier-balanced";
12+
import { RandomPriorityExpansion } from "../../../../../../../baselines/random-priority";
13+
import { StandardBfsExpansion } from "../../../../../../../baselines/standard-bfs";
14+
import { createChainGraphExpander, createHubGraphExpander } from "../../../../common/graph-generators";
15+
16+
describe("VALIDITY: N-Seed Comparison Across Methods", () => {
17+
/**
18+
* Compare all methods across N=1, N=2, N=3 variants on chain graph.
19+
*/
20+
it("should compare all methods across N-seed variants", async () => {
21+
const graph = createChainGraphExpander(10);
22+
const totalNodes = 10;
23+
24+
const results: Array<{
25+
method: string;
26+
n: number;
27+
nodes: number;
28+
paths: number;
29+
iterations: number;
30+
coverage: number;
31+
}> = [];
32+
33+
// Test N=1 (ego-graph) - no paths expected
34+
for (const [method, seeds] of [
35+
["Degree-Prioritised", ["N0"]],
36+
["Standard BFS", ["N0"]],
37+
["Frontier-Balanced", ["N0"]],
38+
["Random Priority", ["N0"]],
39+
] as const) {
40+
let expansion;
41+
switch (method) {
42+
case "Degree-Prioritised": {
43+
expansion = new DegreePrioritisedExpansion(graph, seeds);
44+
break;
45+
}
46+
case "Standard BFS": {
47+
expansion = new StandardBfsExpansion(graph, seeds);
48+
break;
49+
}
50+
case "Frontier-Balanced": {
51+
expansion = new FrontierBalancedExpansion(graph, seeds);
52+
break;
53+
}
54+
case "Random Priority": {
55+
expansion = new RandomPriorityExpansion(graph, seeds, 42);
56+
break;
57+
}
58+
}
59+
60+
const result = await expansion.run();
61+
results.push({
62+
method,
63+
n: 1,
64+
nodes: result.sampledNodes.size,
65+
paths: result.paths.length,
66+
iterations: result.stats.iterations,
67+
coverage: (result.sampledNodes.size / totalNodes) * 100,
68+
});
69+
}
70+
71+
// Test N=2 (between-graph) - paths expected
72+
for (const [method, seeds] of [
73+
["Degree-Prioritised", ["N0", "N9"]],
74+
["Standard BFS", ["N0", "N9"]],
75+
["Frontier-Balanced", ["N0", "N9"]],
76+
["Random Priority", ["N0", "N9"]],
77+
] as const) {
78+
let expansion;
79+
switch (method) {
80+
case "Degree-Prioritised": {
81+
expansion = new DegreePrioritisedExpansion(graph, seeds);
82+
break;
83+
}
84+
case "Standard BFS": {
85+
expansion = new StandardBfsExpansion(graph, seeds);
86+
break;
87+
}
88+
case "Frontier-Balanced": {
89+
expansion = new FrontierBalancedExpansion(graph, seeds);
90+
break;
91+
}
92+
case "Random Priority": {
93+
expansion = new RandomPriorityExpansion(graph, seeds, 42);
94+
break;
95+
}
96+
}
97+
98+
const result = await expansion.run();
99+
results.push({
100+
method,
101+
n: 2,
102+
nodes: result.sampledNodes.size,
103+
paths: result.paths.length,
104+
iterations: result.stats.iterations,
105+
coverage: (result.sampledNodes.size / totalNodes) * 100,
106+
});
107+
}
108+
109+
// Test N=3 on hub graph (different structure)
110+
const hubGraph = createHubGraphExpander(3, 5);
111+
const hubTotalNodes = 16; // 3 hubs + 3*5 spokes + 1 connector per hub = 18 approximately
112+
113+
for (const [method, seeds] of [
114+
["Degree-Prioritised", ["L0_0", "L1_2", "L2_4"]],
115+
["Standard BFS", ["L0_0", "L1_2", "L2_4"]],
116+
["Frontier-Balanced", ["L0_0", "L1_2", "L2_4"]],
117+
["Random Priority", ["L0_0", "L1_2", "L2_4"]],
118+
] as const) {
119+
let expansion;
120+
switch (method) {
121+
case "Degree-Prioritised": {
122+
expansion = new DegreePrioritisedExpansion(hubGraph, seeds);
123+
break;
124+
}
125+
case "Standard BFS": {
126+
expansion = new StandardBfsExpansion(hubGraph, seeds);
127+
break;
128+
}
129+
case "Frontier-Balanced": {
130+
expansion = new FrontierBalancedExpansion(hubGraph, seeds);
131+
break;
132+
}
133+
case "Random Priority": {
134+
expansion = new RandomPriorityExpansion(hubGraph, seeds, 42);
135+
break;
136+
}
137+
}
138+
139+
const result = await expansion.run();
140+
results.push({
141+
method,
142+
n: 3,
143+
nodes: result.sampledNodes.size,
144+
paths: result.paths.length,
145+
iterations: result.stats.iterations,
146+
coverage: (result.sampledNodes.size / hubTotalNodes) * 100,
147+
});
148+
}
149+
150+
// Verify all methods found nodes
151+
for (const r of results) {
152+
expect(r.nodes).toBeGreaterThan(0);
153+
expect(r.iterations).toBeGreaterThan(0);
154+
}
155+
156+
// Output comparison table
157+
console.log("\n=== N-Seed Comparison Across Methods ===");
158+
console.log("Method & N & Nodes & Paths & Iterations & Coverage");
159+
for (const r of results) {
160+
const coverageString = r.coverage.toFixed(1) + "%";
161+
console.log(`${r.method} & ${r.n} & ${r.nodes} & ${r.paths} & ${r.iterations} & ${coverageString}`);
162+
}
163+
});
164+
165+
/**
166+
* Compare hub traversal rates across methods for N=2 variant.
167+
*/
168+
it("should compare hub traversal across methods", async () => {
169+
const graph = createHubGraphExpander(3, 5);
170+
const seeds: [string, string] = ["L0_0", "L2_4"];
171+
172+
// Define top 10% degree threshold for hub identification
173+
const allDegrees = new Map<string, number>();
174+
for (let index = 0; index < 20; index++) {
175+
try {
176+
const nodeId = `L${Math.floor(index / 5)}_${index % 5}`;
177+
allDegrees.set(nodeId, graph.getDegree(nodeId));
178+
} catch {
179+
// Node may not exist
180+
}
181+
}
182+
183+
const degrees = [...allDegrees.values()].sort((a, b) => b - a);
184+
const hubThreshold = degrees[Math.floor(degrees.length * 0.1)] || 5;
185+
const hubs = new Set<string>();
186+
for (const [node, degree] of allDegrees) {
187+
if (degree >= hubThreshold) {
188+
hubs.add(node);
189+
}
190+
}
191+
192+
const results: Array<{
193+
method: string;
194+
paths: number;
195+
hubTraversal: number;
196+
}> = [];
197+
198+
for (const [method, ctor] of [
199+
["Degree-Prioritised", (seeds: [string, string]) => new DegreePrioritisedExpansion(graph, seeds)],
200+
["Standard BFS", (seeds: [string, string]) => new StandardBfsExpansion(graph, seeds)],
201+
["Frontier-Balanced", (seeds: [string, string]) => new FrontierBalancedExpansion(graph, seeds)],
202+
["Random Priority", (seeds: [string, string]) => new RandomPriorityExpansion(graph, seeds, 42)],
203+
] as const) {
204+
const expansion = ctor(seeds);
205+
const result = await expansion.run();
206+
207+
// Calculate hub traversal rate
208+
let pathsWithHubs = 0;
209+
for (const path of result.paths) {
210+
const hasHub = path.nodes.some((nodeId) => hubs.has(nodeId));
211+
if (hasHub) pathsWithHubs++;
212+
}
213+
214+
const hubTraversal = result.paths.length > 0
215+
? (pathsWithHubs / result.paths.length) * 100
216+
: 0;
217+
218+
results.push({
219+
method,
220+
paths: result.paths.length,
221+
hubTraversal: Math.round(hubTraversal),
222+
});
223+
}
224+
225+
// Output hub traversal comparison
226+
console.log("\n=== N=2 Hub Traversal Comparison ===");
227+
console.log("Method & Paths & Hub Traversal");
228+
for (const r of results) {
229+
console.log(`${r.method} & ${r.paths} & ${r.hubTraversal}%`);
230+
}
231+
232+
// All methods should find paths
233+
expect(results.length).toBe(4);
234+
for (const r of results) {
235+
expect(r.paths).toBeGreaterThan(0);
236+
}
237+
});
238+
239+
/**
240+
* Compare path diversity across methods for N=2 variant.
241+
*/
242+
it("should compare path diversity across methods", async () => {
243+
const graph = createHubGraphExpander(3, 5);
244+
const seeds: [string, string] = ["L0_0", "L2_4"];
245+
246+
const results: Array<{
247+
method: string;
248+
paths: number;
249+
uniqueNodes: number;
250+
diversity: number;
251+
}> = [];
252+
253+
for (const [method, ctor] of [
254+
["Degree-Prioritised", (seeds: [string, string]) => new DegreePrioritisedExpansion(graph, seeds)],
255+
["Standard BFS", (seeds: [string, string]) => new StandardBfsExpansion(graph, seeds)],
256+
["Frontier-Balanced", (seeds: [string, string]) => new FrontierBalancedExpansion(graph, seeds)],
257+
["Random Priority", (seeds: [string, string]) => new RandomPriorityExpansion(graph, seeds, 42)],
258+
] as const) {
259+
const expansion = ctor(seeds);
260+
const result = await expansion.run();
261+
262+
// Calculate path diversity (Jaccard dissimilarity)
263+
const pathNodeSets = result.paths.map((p) => new Set(p.nodes));
264+
const allNodes = new Set<string>();
265+
for (const set of pathNodeSets) {
266+
for (const node of set) {
267+
allNodes.add(node);
268+
}
269+
}
270+
271+
let totalJaccard = 0;
272+
let comparisons = 0;
273+
for (let index = 0; index < pathNodeSets.length; index++) {
274+
for (let index_ = index + 1; index_ < pathNodeSets.length; index_++) {
275+
const intersection = new Set<string>();
276+
for (const node of pathNodeSets[index]) {
277+
if (pathNodeSets[index_].has(node)) {
278+
intersection.add(node);
279+
}
280+
}
281+
const union = new Set([...pathNodeSets[index], ...pathNodeSets[index_]]);
282+
const jaccard = intersection.size / union.size;
283+
totalJaccard += 1 - jaccard; // Dissimilarity
284+
comparisons++;
285+
}
286+
}
287+
288+
const diversity = comparisons > 0 ? totalJaccard / comparisons : 0;
289+
290+
results.push({
291+
method,
292+
paths: result.paths.length,
293+
uniqueNodes: allNodes.size,
294+
diversity: Number.parseFloat(diversity.toFixed(3)),
295+
});
296+
}
297+
298+
// Output diversity comparison
299+
console.log("\n=== N=2 Path Diversity Comparison ===");
300+
console.log("Method & Paths & Unique Nodes & Diversity");
301+
for (const r of results) {
302+
console.log(`${r.method} & ${r.paths} & ${r.uniqueNodes} & ${r.diversity}`);
303+
}
304+
305+
// All methods should find paths with valid diversity
306+
expect(results.length).toBe(4);
307+
for (const r of results) {
308+
expect(r.paths).toBeGreaterThan(0);
309+
expect(r.uniqueNodes).toBeGreaterThan(0);
310+
expect(r.diversity).toBeGreaterThanOrEqual(0);
311+
expect(r.diversity).toBeLessThanOrEqual(1);
312+
}
313+
});
314+
});

0 commit comments

Comments
 (0)