|
| 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