Skip to content

Commit 4fc065e

Browse files
committed
refactor(graph-gen): break up generator.ts into thematic modules
Split generator.ts (4,401 → 1,650 lines, 62.5% reduction) into 11 thematic modules: - types.ts: Shared types and SeededRandom class - core-structures.ts: Trees, stars, grids, tournaments, regular graphs - bipartite.ts: Complete bipartite, bipartite trees/forests - connectivity.ts: Flow networks, Eulerian, k-vertex/k-edge connected - structural-classes.ts: Split, cograph, chordal, interval, perfect - symmetry.ts: Strongly regular, vertex-transitive, self-complementary - network-structures.ts: Scale-free, small-world, modular - geometric.ts: Unit disk, planar - path-cycle.ts: Hamiltonian, traceable, diameter, girth - spectral.ts: Spectrum, algebraic connectivity, spectral radius - invariants.ts: Independence number, vertex cover, domination Updated imports in validation files to reference new types module. All tests passing.
1 parent c223359 commit 4fc065e

File tree

14 files changed

+3843
-3243
lines changed

14 files changed

+3843
-3243
lines changed

packages/graph-gen/src/generator.ts

Lines changed: 490 additions & 3241 deletions
Large diffs are not rendered by default.
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
/**
2+
* Bipartite graph generators
3+
*
4+
* Functions for generating various bipartite graph structures:
5+
* - Complete bipartite K_{m,n}
6+
* - Bipartite trees (acyclic connected)
7+
* - Connected bipartite graphs with even-length cycles
8+
* - Bipartite forests (acyclic disconnected)
9+
* - Disconnected bipartite graphs with cycles
10+
*/
11+
12+
import type { GraphSpec } from '../spec';
13+
import type { TestNode, TestEdge } from './types';
14+
import { SeededRandom } from './types';
15+
16+
/**
17+
* Get nodes in left and right partitions for bipartite graphs.
18+
* @param nodes
19+
*/
20+
const getBipartitePartitions = (nodes: TestNode[]): { left: TestNode[]; right: TestNode[] } => {
21+
const left = nodes.filter((node): node is TestNode & { partition: 'left' } => node.partition === "left");
22+
const right = nodes.filter((node): node is TestNode & { partition: 'right' } => node.partition === "right");
23+
return { left, right };
24+
};
25+
26+
/**
27+
* Add edge to edge list, handling heterogeneous schema types.
28+
* @param edges - Edge list to modify
29+
* @param source - Source node ID
30+
* @param target - Target node ID
31+
* @param spec - Graph specification
32+
* @param rng - Seeded random number generator
33+
*/
34+
const addEdge = (edges: TestEdge[], source: string, target: string, spec: GraphSpec, rng: SeededRandom): void => {
35+
const edge: TestEdge = { source, target };
36+
37+
if (spec.schema.kind === 'heterogeneous') {
38+
// Assign random edge type (could be based on config.edgeTypes)
39+
edge.type = rng.choice(['type_a', 'type_b', 'type_c']);
40+
}
41+
42+
edges.push(edge);
43+
};
44+
45+
/**
46+
* Generate complete bipartite K_{m,n} graph.
47+
* @param nodes
48+
* @param edges
49+
* @param spec
50+
* @param rng
51+
*/
52+
export const generateCompleteBipartiteEdges = (nodes: TestNode[], edges: TestEdge[], spec: GraphSpec, rng: SeededRandom): void => {
53+
const { left, right } = getBipartitePartitions(nodes);
54+
55+
// Add all possible edges between left and right partitions
56+
for (const leftNode of left) {
57+
for (const rightNode of right) {
58+
// Both directed and undirected bipartite graphs use the same edge structure
59+
addEdge(edges, leftNode.id, rightNode.id, spec, rng);
60+
}
61+
}
62+
};
63+
64+
/**
65+
* Generate bipartite tree (connected, acyclic bipartite graph).
66+
* @param nodes
67+
* @param edges
68+
* @param spec
69+
* @param rng
70+
*/
71+
export const generateBipartiteTreeEdges = (nodes: TestNode[], edges: TestEdge[], spec: GraphSpec, rng: SeededRandom): void => {
72+
const { left, right } = getBipartitePartitions(nodes);
73+
74+
if (left.length === 0 || right.length === 0) return;
75+
76+
// Start with one edge connecting left to right
77+
const firstLeft = left[0];
78+
const firstRight = right[0];
79+
addEdge(edges, firstLeft.id, firstRight.id, spec, rng);
80+
81+
const connected = new Set([firstLeft.id, firstRight.id]);
82+
83+
// Connect remaining nodes
84+
const allNodes = [...left.slice(1), ...right.slice(1)];
85+
86+
for (const node of allNodes) {
87+
// Connect to a random node in opposite partition that's already connected
88+
const oppositePartition = node.partition === "left" ? right : left;
89+
const connectedOpposite = oppositePartition.filter(n => connected.has(n.id));
90+
91+
if (connectedOpposite.length > 0) {
92+
const target = rng.choice(connectedOpposite);
93+
addEdge(edges, node.id, target.id, spec, rng);
94+
connected.add(node.id);
95+
}
96+
}
97+
};
98+
99+
/**
100+
* Generate connected bipartite graph with even-length cycles.
101+
* @param nodes
102+
* @param edges
103+
* @param spec
104+
* @param rng
105+
*/
106+
export const generateBipartiteConnectedEdges = (nodes: TestNode[], edges: TestEdge[], spec: GraphSpec, rng: SeededRandom): void => {
107+
const { left, right } = getBipartitePartitions(nodes);
108+
109+
if (left.length === 0 || right.length === 0) return;
110+
111+
// First create a spanning tree to ensure connectivity
112+
generateBipartiteTreeEdges(nodes, edges, spec, rng);
113+
114+
// Add extra edges between partitions (creates even-length cycles)
115+
const minPartitionSize = Math.min(left.length, right.length);
116+
const edgesToAdd = Math.max(0, minPartitionSize - 1); // Add some extra edges
117+
118+
for (let i = 0; i < edgesToAdd; i++) {
119+
const source = rng.choice(left);
120+
const target = rng.choice(right);
121+
122+
// Avoid duplicate edges for simple graphs
123+
if (spec.edgeMultiplicity.kind === "simple") {
124+
const exists = edges.some(e =>
125+
(e.source === source.id && e.target === target.id) ||
126+
(spec.directionality.kind === "undirected" && e.source === target.id && e.target === source.id)
127+
);
128+
if (exists) continue;
129+
}
130+
131+
addEdge(edges, source.id, target.id, spec, rng);
132+
}
133+
};
134+
135+
/**
136+
* Generate bipartite forest (disconnected acyclic bipartite graphs).
137+
* @param nodes
138+
* @param edges
139+
* @param spec
140+
* @param rng
141+
*/
142+
export const generateBipartiteForestEdges = (nodes: TestNode[], edges: TestEdge[], spec: GraphSpec, rng: SeededRandom): void => {
143+
const { left, right } = getBipartitePartitions(nodes);
144+
145+
if (left.length === 0 || right.length === 0) return;
146+
147+
// Create multiple tree components
148+
const numComponents = Math.max(2, Math.floor(Math.sqrt(nodes.length)));
149+
const nodesPerComponent = Math.ceil(nodes.length / numComponents);
150+
151+
for (let c = 0; c < numComponents; c++) {
152+
const startIdx = c * nodesPerComponent;
153+
const endIdx = Math.min(startIdx + nodesPerComponent, nodes.length);
154+
const componentNodes = nodes.slice(startIdx, endIdx);
155+
156+
if (componentNodes.length < 2) continue;
157+
158+
// For bipartite, ensure each component has at least one node from each partition
159+
const compLeft = componentNodes.filter((node): node is TestNode & { partition: 'left' } => node.partition === "left");
160+
const compRight = componentNodes.filter((node): node is TestNode & { partition: 'right' } => node.partition === "right");
161+
162+
if (compLeft.length === 0 || compRight.length === 0) continue;
163+
164+
// Create one edge to start the component
165+
addEdge(edges, compLeft[0].id, compRight[0].id, spec, rng);
166+
167+
const connected = new Set([compLeft[0].id, compRight[0].id]);
168+
const remaining = [...compLeft.slice(1), ...compRight.slice(1)];
169+
170+
// Connect rest of component
171+
for (const node of remaining) {
172+
const oppositePartition = node.partition === "left" ? compRight : compLeft;
173+
const connectedOpposite = oppositePartition.filter(n => connected.has(n.id));
174+
175+
if (connectedOpposite.length > 0) {
176+
const target = rng.choice(connectedOpposite);
177+
addEdge(edges, node.id, target.id, spec, rng);
178+
connected.add(node.id);
179+
}
180+
}
181+
}
182+
};
183+
184+
/**
185+
* Generate disconnected bipartite graph with cycles.
186+
* @param nodes
187+
* @param edges
188+
* @param spec
189+
* @param rng
190+
*/
191+
export const generateBipartiteDisconnectedEdges = (nodes: TestNode[], edges: TestEdge[], spec: GraphSpec, rng: SeededRandom): void => {
192+
const { left, right } = getBipartitePartitions(nodes);
193+
194+
if (left.length === 0 || right.length === 0) return;
195+
196+
// Create 2-4 components
197+
const numComponents = 2 + Math.floor(rng.next() * 3);
198+
const nodesPerComponent = Math.ceil(nodes.length / numComponents);
199+
200+
for (let c = 0; c < numComponents; c++) {
201+
const startIdx = c * nodesPerComponent;
202+
const endIdx = Math.min(startIdx + nodesPerComponent, nodes.length);
203+
const componentNodes = nodes.slice(startIdx, endIdx);
204+
205+
const compLeft = componentNodes.filter((node): node is TestNode & { partition: 'left' } => node.partition === "left");
206+
const compRight = componentNodes.filter((node): node is TestNode & { partition: 'right' } => node.partition === "right");
207+
208+
if (compLeft.length === 0 || compRight.length === 0) continue;
209+
210+
// Ensure connectivity within component
211+
const connected = new Set();
212+
const firstLeft = compLeft[0];
213+
const firstRight = compRight[0];
214+
addEdge(edges, firstLeft.id, firstRight.id, spec, rng);
215+
connected.add(firstLeft.id);
216+
connected.add(firstRight.id);
217+
218+
// Add edges to connect rest of component
219+
for (const node of [...compLeft.slice(1), ...compRight.slice(1)]) {
220+
const oppositePartition = node.partition === "left" ? compRight : compLeft;
221+
const connectedOpposite = oppositePartition.filter(n => connected.has(n.id));
222+
223+
if (connectedOpposite.length > 0) {
224+
const target = rng.choice(connectedOpposite);
225+
addEdge(edges, node.id, target.id, spec, rng);
226+
connected.add(node.id);
227+
}
228+
}
229+
230+
// Add some extra edges to create cycles (even-length for bipartite)
231+
const extraEdges = Math.floor(rng.next() * compLeft.length);
232+
for (let i = 0; i < extraEdges; i++) {
233+
const source = rng.choice(compLeft);
234+
const target = rng.choice(compRight);
235+
236+
if (spec.edgeMultiplicity.kind === "simple") {
237+
const exists = edges.some(e =>
238+
(e.source === source.id && e.target === target.id) ||
239+
(spec.directionality.kind === "undirected" && e.source === target.id && e.target === source.id)
240+
);
241+
if (exists) continue;
242+
}
243+
244+
addEdge(edges, source.id, target.id, spec, rng);
245+
}
246+
}
247+
};

0 commit comments

Comments
 (0)