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