Skip to content

Commit 7b2d6d0

Browse files
committed
feat: add graph generation system
Implement type-safe graph specification and generation: - Graph specification DSL with validation - Constraint system for graph properties - Generator for creating graphs from specifications - Validators for ensuring generated graphs meet constraints
1 parent 7b93ee6 commit 7b2d6d0

35 files changed

Lines changed: 7537 additions & 0 deletions

src/generation/constraints.ts

Lines changed: 382 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,382 @@
1+
import type { GraphSpec } from "./spec";
2+
3+
/**
4+
* Mathematical impossibility in a graph specification.
5+
*/
6+
export interface GraphSpecImpossibility {
7+
property: string;
8+
reason: string;
9+
severity: "error" | "warning";
10+
}
11+
12+
/**
13+
* Analyze a graph spec for mathematically impossible combinations.
14+
* @param spec
15+
*/
16+
export const analyzeGraphSpecConstraints = (spec: GraphSpec): GraphSpecImpossibility[] => {
17+
const impossibilities: GraphSpecImpossibility[] = [];
18+
19+
// 1. Complete graphs must be connected
20+
if (spec.completeness.kind === "complete" && spec.connectivity.kind === "unconstrained") {
21+
impossibilities.push({
22+
property: "connectivity/completeness",
23+
reason: "Complete graphs are inherently connected (every node reachable from every other)",
24+
severity: "error"
25+
});
26+
}
27+
28+
// 2. Acyclic + Complete is impossible for n > 2
29+
if (spec.completeness.kind === "complete" && spec.cycles.kind === "acyclic") {
30+
impossibilities.push({
31+
property: "completeness/cycles",
32+
reason: "Complete graphs contain cycles (n*(n-1) edges creates many cycles)",
33+
severity: "error"
34+
});
35+
}
36+
37+
// 3. Multigraph + Acyclic + Disconnected is problematic
38+
// Forests (acyclic disconnected) are inherently simple graphs
39+
if (spec.edgeMultiplicity.kind === "multi" &&
40+
spec.cycles.kind === "acyclic" &&
41+
spec.connectivity.kind === "unconstrained") {
42+
impossibilities.push({
43+
property: "edgeMultiplicity/cycles/connectivity",
44+
reason: "Forests (acyclic disconnected graphs) are inherently simple; parallel edges either create cycles or are redundant",
45+
severity: "warning"
46+
});
47+
}
48+
49+
// 4. Density constraints for forests
50+
// For acyclic disconnected graphs (forests), minimum density is (n-k)/maxEdges
51+
// where k is number of components. This can exceed "sparse" threshold.
52+
if (spec.cycles.kind === "acyclic" &&
53+
spec.connectivity.kind === "unconstrained" &&
54+
spec.density.kind === "sparse") {
55+
impossibilities.push({
56+
property: "cycles/density/connectivity",
57+
reason: "Forest minimum density may exceed sparse threshold depending on component structure",
58+
severity: "warning"
59+
});
60+
}
61+
62+
// 5. Tree density constraints
63+
// Trees (acyclic connected) have exactly n-1 edges, which may not match density spec
64+
if (spec.directionality.kind === "undirected" &&
65+
spec.cycles.kind === "acyclic" &&
66+
spec.connectivity.kind === "connected" &&
67+
spec.density.kind !== "unconstrained") {
68+
impossibilities.push({
69+
property: "cycles/density",
70+
reason: "Trees have exactly n-1 edges, which may not match specified density",
71+
severity: "warning"
72+
});
73+
}
74+
75+
// 6. Self-loops in acyclic graphs
76+
// Self-loops are cycles in directed graphs
77+
if (spec.cycles.kind === "acyclic" &&
78+
spec.selfLoops.kind === "allowed" &&
79+
spec.directionality.kind === "directed") {
80+
impossibilities.push({
81+
property: "cycles/selfLoops",
82+
reason: "Self-loops create cycles in directed graphs",
83+
severity: "error"
84+
});
85+
}
86+
87+
// 7. Multigraph + Acyclic + Connected + Undirected is IMPOSSIBLE
88+
// A connected acyclic undirected graph is a tree, which cannot have parallel edges
89+
if (spec.edgeMultiplicity.kind === "multi" &&
90+
spec.cycles.kind === "acyclic" &&
91+
spec.connectivity.kind === "connected" &&
92+
spec.directionality.kind === "undirected") {
93+
impossibilities.push({
94+
property: "edgeMultiplicity/cycles/connectivity/directionality",
95+
reason: "Undirected connected acyclic graphs are trees (n-1 edges, no parallel edges). Multigraphs require parallel edges which would create cycles.",
96+
severity: "error"
97+
});
98+
}
99+
100+
// 8. Multigraph + Acyclic for undirected graphs is IMPOSSIBLE
101+
// Undirected acyclic graphs (forests/trees) cannot have parallel edges without creating cycles
102+
if (spec.directionality.kind === "undirected" &&
103+
spec.edgeMultiplicity.kind === "multi" &&
104+
spec.cycles.kind === "acyclic") {
105+
impossibilities.push({
106+
property: "edgeMultiplicity/cycles/directionality",
107+
reason: "Undirected acyclic graphs (forests and trees) cannot have parallel edges. Adding parallel edges would create cycles.",
108+
severity: "error"
109+
});
110+
}
111+
112+
// 9. Multigraph + Acyclic for directed graphs is problematic but may be possible
113+
// Directed acyclic graphs can have parallel edges in opposite directions without creating cycles
114+
if (spec.directionality.kind === "directed" &&
115+
spec.edgeMultiplicity.kind === "multi" &&
116+
spec.cycles.kind === "acyclic") {
117+
impossibilities.push({
118+
property: "edgeMultiplicity/cycles/directionality",
119+
reason: "Directed acyclic graphs with parallel edges require careful design to avoid cycles",
120+
severity: "warning"
121+
});
122+
}
123+
124+
// 8. Bipartite + cycles with odd length is impossible
125+
// Bipartite graphs cannot contain odd-length cycles
126+
if (spec.partiteness?.kind === "bipartite" &&
127+
spec.cycles.kind === "cycles_allowed" &&
128+
spec.directionality.kind === "undirected") {
129+
impossibilities.push({
130+
property: "partiteness/cycles",
131+
reason: "Bipartite graphs cannot contain odd-length cycles (all cycles in bipartite graphs have even length)",
132+
severity: "warning"
133+
});
134+
}
135+
136+
// 9. Planar + Complete graph with n ≥ 5 is impossible
137+
// K5 is non-planar (Kuratowski's theorem)
138+
if (spec.embedding?.kind === "planar" &&
139+
spec.completeness.kind === "complete") {
140+
impossibilities.push({
141+
property: "embedding/completeness",
142+
reason: "Complete graphs with n ≥ 5 are non-planar (K5 is Kuratowski's first graph)",
143+
severity: "error"
144+
});
145+
}
146+
147+
// 10. Planar + Complete bipartite K3,3 or larger is impossible
148+
if (spec.embedding?.kind === "planar" &&
149+
spec.completeBipartite?.kind === "complete_bipartite") {
150+
const { m, n } = spec.completeBipartite;
151+
if (m >= 3 && n >= 3) {
152+
impossibilities.push({
153+
property: "embedding/completeBipartite",
154+
reason: `K${m},${n} is non-planar when m,n ≥ 3 (K3,3 is Kuratowski's second graph)`,
155+
severity: "error"
156+
});
157+
}
158+
}
159+
160+
// 11. k-vertex-connected requires at least k+1 vertices
161+
if (spec.kVertexConnected?.kind === "k_vertex_connected") {
162+
const { k } = spec.kVertexConnected;
163+
// We can't check node count here (it's a generation parameter, not a spec property)
164+
// But we can document the constraint for validation
165+
impossibilities.push({
166+
property: "kVertexConnected/nodeCount",
167+
reason: `k-vertex-connected graphs require at least ${k + 1} vertices (will be validated during generation)`,
168+
severity: "warning"
169+
});
170+
}
171+
172+
// 12. k-edge-connected requires at least k+1 vertices
173+
if (spec.kEdgeConnected?.kind === "k_edge_connected") {
174+
const { k } = spec.kEdgeConnected;
175+
impossibilities.push({
176+
property: "kEdgeConnected/nodeCount",
177+
reason: `k-edge-connected graphs require at least ${k + 1} vertices (will be validated during generation)`,
178+
severity: "warning"
179+
});
180+
}
181+
182+
// 13. Perfect matching + odd vertex count is impossible for simple graphs
183+
if (spec.perfectMatching?.kind === "perfect_matching" &&
184+
spec.edgeMultiplicity.kind === "simple") {
185+
impossibilities.push({
186+
property: "perfectMatching/vertexCount",
187+
reason: "Simple graphs with odd vertex count cannot have perfect matching (will be validated during generation)",
188+
severity: "warning"
189+
});
190+
}
191+
192+
// 14. k-colorable + chromatic number > k is impossible
193+
if (spec.kColorable?.kind === "k_colorable" &&
194+
spec.chromaticNumber?.kind === "chromatic_number") {
195+
const { k: colorableK } = spec.kColorable;
196+
const { chi } = spec.chromaticNumber;
197+
if (chi > colorableK) {
198+
impossibilities.push({
199+
property: "kColorable/chromaticNumber",
200+
reason: `Graph cannot be ${colorableK}-colorable if chromatic number is ${chi} (chi > k)`,
201+
severity: "error"
202+
});
203+
}
204+
}
205+
206+
// 15. Planar graphs have treewidth ≤ 4
207+
if (spec.embedding?.kind === "planar" &&
208+
spec.treewidth?.kind === "treewidth") {
209+
const { width } = spec.treewidth;
210+
if (width > 4) {
211+
impossibilities.push({
212+
property: "embedding/treewidth",
213+
reason: `Planar graphs have treewidth ≤ 4, but spec requires treewidth ${width}`,
214+
severity: "error"
215+
});
216+
}
217+
}
218+
219+
// 16. Tournament graphs must be directed
220+
if (spec.tournament?.kind === "tournament" &&
221+
spec.directionality.kind === "undirected") {
222+
impossibilities.push({
223+
property: "tournament/directionality",
224+
reason: "Tournament graphs are complete oriented graphs (must be directed)",
225+
severity: "error"
226+
});
227+
}
228+
229+
// 17. Tournament + Complete (redundant)
230+
if (spec.tournament?.kind === "tournament" &&
231+
spec.completeness.kind === "complete") {
232+
impossibilities.push({
233+
property: "tournament/completeness",
234+
reason: "Tournament graphs are inherently complete (one directed edge between each vertex pair)",
235+
severity: "warning"
236+
});
237+
}
238+
239+
// 18. Comparability graphs are perfect
240+
if (spec.comparability?.kind === "comparability" &&
241+
spec.perfect?.kind === "imperfect") {
242+
impossibilities.push({
243+
property: "comparability/perfect",
244+
reason: "Comparability graphs are perfect (cannot be imperfect)",
245+
severity: "error"
246+
});
247+
}
248+
249+
// 19. Interval graphs are chordal and perfect
250+
if (spec.interval?.kind === "interval") {
251+
if (spec.chordal?.kind === "non_chordal") {
252+
impossibilities.push({
253+
property: "interval/chordal",
254+
reason: "Interval graphs are chordal (cannot be non-chordal)",
255+
severity: "error"
256+
});
257+
}
258+
if (spec.perfect?.kind === "imperfect") {
259+
impossibilities.push({
260+
property: "interval/perfect",
261+
reason: "Interval graphs are perfect (cannot be imperfect)",
262+
severity: "error"
263+
});
264+
}
265+
}
266+
267+
// 20. Chordal graphs are perfect
268+
if (spec.chordal?.kind === "chordal" &&
269+
spec.perfect?.kind === "imperfect") {
270+
impossibilities.push({
271+
property: "chordal/perfect",
272+
reason: "Chordal graphs are perfect (cannot be imperfect)",
273+
severity: "error"
274+
});
275+
}
276+
277+
// 21. Bipartite graphs are 2-colorable
278+
if (spec.partiteness?.kind === "bipartite" &&
279+
spec.kColorable?.kind === "k_colorable") {
280+
const { k } = spec.kColorable;
281+
if (k < 2) {
282+
impossibilities.push({
283+
property: "partiteness/kColorable",
284+
reason: `Bipartite graphs are 2-colorable, but spec requires ${k}-colorable (k < 2)`,
285+
severity: "error"
286+
});
287+
}
288+
}
289+
290+
// 22. Star graphs are trees
291+
if (spec.star?.kind === "star" &&
292+
spec.cycles.kind === "cycles_allowed") {
293+
impossibilities.push({
294+
property: "star/cycles",
295+
reason: "Star graphs are trees (cannot have cycles)",
296+
severity: "error"
297+
});
298+
}
299+
300+
// 23. Grid graphs are bipartite
301+
// Grid graphs are inherently bipartite, so specifying both is compatible (no constraint needed)
302+
// This is just a note for documentation purposes
303+
304+
// 24. Binary trees are trees
305+
if ((spec.binaryTree?.kind === "binary_tree" ||
306+
spec.binaryTree?.kind === "full_binary" ||
307+
spec.binaryTree?.kind === "complete_binary") &&
308+
spec.cycles.kind === "cycles_allowed") {
309+
impossibilities.push({
310+
property: "binaryTree/cycles",
311+
reason: "Binary trees are trees (cannot have cycles)",
312+
severity: "error"
313+
});
314+
}
315+
316+
// 25. Eulerian circuit requires all vertices have even degree
317+
// Semi-Eulerian requires exactly 2 vertices have odd degree
318+
// These will be validated during generation, but we can document the constraint
319+
if (spec.eulerian?.kind === "eulerian" ||
320+
spec.eulerian?.kind === "semi_eulerian") {
321+
impossibilities.push({
322+
property: "eulerian/degree",
323+
reason: "Eulerian graphs require specific degree constraints (validated during generation)",
324+
severity: "warning"
325+
});
326+
}
327+
328+
// 26. Flow networks require directed graphs
329+
if (spec.flowNetwork?.kind === "flow_network" &&
330+
spec.directionality.kind === "undirected") {
331+
impossibilities.push({
332+
property: "flowNetwork/directionality",
333+
reason: "Flow networks require directed edges (to define source → sink flow)",
334+
severity: "error"
335+
});
336+
}
337+
338+
return impossibilities;
339+
};
340+
341+
/**
342+
* Check if a graph spec combination is mathematically impossible.
343+
* @param spec
344+
*/
345+
export const isGraphSpecImpossible = (spec: GraphSpec): boolean => {
346+
const impossibilities = analyzeGraphSpecConstraints(spec);
347+
return impossibilities.some(imp => imp.severity === "error");
348+
};
349+
350+
/**
351+
* Get adjusted validation expectations for impossible combinations.
352+
* For specs with warnings, relax certain validation constraints.
353+
* @param spec
354+
*/
355+
export const getAdjustedValidationExpectations = (spec: GraphSpec): Partial<Record<string, boolean>> => {
356+
const adjustments: Partial<Record<string, boolean>> = {};
357+
const impossibilities = analyzeGraphSpecConstraints(spec);
358+
359+
for (const imp of impossibilities) {
360+
if (imp.severity === "warning") {
361+
// For warnings, adjust validation expectations
362+
363+
// Multigraph + Acyclic: Don't validate cycles (parallel edges don't create traditional cycles)
364+
if (imp.property.includes("edgeMultiplicity/cycles")) {
365+
// The validator should accept acyclic for multigraphs even if cycles allowed
366+
adjustments["skipCycleValidation"] = true;
367+
}
368+
369+
// Forest density: Accept any density that's the minimum possible for the structure
370+
if (imp.property.includes("cycles/density/connectivity")) {
371+
adjustments["relaxDensityValidation"] = true;
372+
}
373+
374+
// Tree density: Accept tree structure regardless of density spec
375+
if (imp.property.includes("cycles/density")) {
376+
adjustments["relaxDensityValidation"] = true;
377+
}
378+
}
379+
}
380+
381+
return adjustments;
382+
};

0 commit comments

Comments
 (0)