/** * Random Tree Generator * * Generates a random tree with 1–MAX_CHILDREN children per node. * Splits edges into primary (depth ≤ PRIMARY_DEPTH) and secondary. * * Usage: npx tsx scripts/generate_tree.ts */ import { writeFileSync } from "fs"; import { join, dirname } from "path"; import { fileURLToPath } from "url"; const __dirname = dirname(fileURLToPath(import.meta.url)); const PUBLIC_DIR = join(__dirname, "..", "public"); // ══════════════════════════════════════════════════════════ // Configuration // ══════════════════════════════════════════════════════════ const TARGET_NODES = 10000; // Approximate number of nodes to generate const MAX_CHILDREN = 4; // Each node gets 1..MAX_CHILDREN children const PRIMARY_DEPTH = 3; // Nodes at depth ≤ this form the primary skeleton // ══════════════════════════════════════════════════════════ // Tree data types // ══════════════════════════════════════════════════════════ export interface TreeData { root: number; nodeCount: number; childrenOf: Map; parentOf: Map; depthOf: Map; primaryNodes: Set; // all nodes at depth ≤ PRIMARY_DEPTH primaryEdges: Array<[number, number]>; // [child, parent] edges within primary secondaryEdges: Array<[number, number]>;// remaining edges } // ══════════════════════════════════════════════════════════ // Generator // ══════════════════════════════════════════════════════════ export function generateTree(): TreeData { const childrenOf = new Map(); const parentOf = new Map(); const depthOf = new Map(); const root = 0; depthOf.set(root, 0); let nextId = 1; const queue: number[] = [root]; let head = 0; while (head < queue.length && nextId < TARGET_NODES) { const parent = queue[head++]; const parentDepth = depthOf.get(parent)!; const nKids = 1 + Math.floor(Math.random() * MAX_CHILDREN); // 1..MAX_CHILDREN const kids: number[] = []; for (let c = 0; c < nKids && nextId < TARGET_NODES; c++) { const child = nextId++; kids.push(child); parentOf.set(child, parent); depthOf.set(child, parentDepth + 1); queue.push(child); } childrenOf.set(parent, kids); } // Classify edges and nodes by depth const primaryNodes = new Set(); const primaryEdges: Array<[number, number]> = []; const secondaryEdges: Array<[number, number]> = []; // Root is always primary primaryNodes.add(root); for (const [child, parent] of parentOf) { const childDepth = depthOf.get(child)!; if (childDepth <= PRIMARY_DEPTH) { primaryNodes.add(child); primaryNodes.add(parent); primaryEdges.push([child, parent]); } else { secondaryEdges.push([child, parent]); } } console.log( `Generated tree: ${nextId} nodes, ` + `${primaryEdges.length} primary edges (depth ≤ ${PRIMARY_DEPTH}), ` + `${secondaryEdges.length} secondary edges` ); return { root, nodeCount: nextId, childrenOf, parentOf, depthOf, primaryNodes, primaryEdges, secondaryEdges, }; } // ══════════════════════════════════════════════════════════ // Run if executed directly // ══════════════════════════════════════════════════════════ if (import.meta.url === `file://${process.argv[1]}`) { const data = generateTree(); // Write primary_edges.csv const pLines: string[] = ["source,target"]; for (const [child, parent] of data.primaryEdges) { pLines.push(`${child},${parent}`); } const pPath = join(PUBLIC_DIR, "primary_edges.csv"); writeFileSync(pPath, pLines.join("\n") + "\n"); console.log(`Wrote ${data.primaryEdges.length} edges to ${pPath}`); // Write secondary_edges.csv const sLines: string[] = ["source,target"]; for (const [child, parent] of data.secondaryEdges) { sLines.push(`${child},${parent}`); } const sPath = join(PUBLIC_DIR, "secondary_edges.csv"); writeFileSync(sPath, sLines.join("\n") + "\n"); console.log(`Wrote ${data.secondaryEdges.length} edges to ${sPath}`); }