diff --git a/Mathilda_spec.md b/Mathilda_spec.md index 4b8e2e15..84af0f31 100644 --- a/Mathilda_spec.md +++ b/Mathilda_spec.md @@ -51,6 +51,7 @@ Each category lives in [`docs/spec/builtins/`](docs/spec/builtins/): | Pattern matching (`MatchQ`, `Cases`, `DeleteCases`, `Position`, `Count`, ...) | [`builtins/pattern-matching.md`](docs/spec/builtins/pattern-matching.md) | | File I/O (`Get`, `Put`, `PutAppend`, `>>`, `>>>`) | [`builtins/file-io.md`](docs/spec/builtins/file-io.md) | | Graphics (`Graphics`, `Show`, `Plot`, `Point`, `Line`, `Rectangle`, `Circle`, `Disk`, `Polygon`, `Text`, ...) | [`builtins/graphics.md`](docs/spec/builtins/graphics.md) | +| Graphs (`Graph`, `VertexList`, `EdgeList`, `AdjacencyMatrix`, `FindShortestPath`, `ConnectedComponents`, `PageRank`, `GraphPlot`, ...) | [`builtins/graphs.md`](docs/spec/builtins/graphs.md) | | FLINT context (`` FLINT`PolynomialGCD ``, `` FLINT`Factor ``, `` FLINT`Det ``, `` FLINT`Zeta ``, ...) — direct access to the FLINT-backed kernels | [`builtins/flint.md`](docs/spec/builtins/flint.md) | ## Changelog diff --git a/SPEC.md b/SPEC.md index 1626eec5..8fea6af1 100644 --- a/SPEC.md +++ b/SPEC.md @@ -63,6 +63,14 @@ src/ poly/ Polynomial subsystem (univariate, multivariate, factoring, algebraic-number factoring, polynomial solving) linalg/ Dense linear algebra; eigen kernels split by algorithm + graph/ Graph subsystem: Graph[] construction/validation, queries + (VertexList/EdgeList/degree/AdjacencyList), matrix views + (Adjacency/Incidence/Kirchhoff/Distance), generators + (Complete/Cycle/Path/Petersen/Random/...), algorithms + (shortest path, components, spanning tree, connectivity, + Euler/Hamilton, cliques, colouring), centrality (PageRank, + Katz, betweenness, clustering) and GraphPlot/Graph3D + (one builtin per file, mirrors linalg/) simp/ Simplify, trig simplification, trig rationalisation calculus/ D / Dt / Derivative, Series, Limit, Integrate, Risch-Norman special_functions/ Higher transcendental & special functions: Gamma, LogGamma, @@ -265,7 +273,8 @@ return arg0; `*_init()` function: `parfrac`, `modular`, `facint`, `comparisons`, `boolean`, `list`, `replace`, `patterns`, `cond`, `iter`, `complex`, `trig`, `simp`, `hyperbolic`, `logexp`, `piecewise`, `attr`, `purefunc`, `stats`, `poly`, -`facpoly`, `rat`, `expand`, `info`, `datetime`, `linalg`, `load`, `graphics`. +`facpoly`, `rat`, `expand`, `info`, `datetime`, `linalg`, `load`, `graphics`, +`graph`. After C-side init, `main()` loads `src/internal/init.m`, which `Get[]`s the remaining `.m` bootstrap files. diff --git a/docs/spec/builtins/graphs.md b/docs/spec/builtins/graphs.md new file mode 100644 index 00000000..b01c9970 --- /dev/null +++ b/docs/spec/builtins/graphs.md @@ -0,0 +1,657 @@ +# Graphs + +A graph subsystem modeled on the Wolfram Language's, implemented in +`src/graph/` (one builtin per translation unit, mirroring `src/linalg/`). +Graphs are ordinary `Expr` trees — there is **no new `EXPR_*` tag** — of the +canonical form + +``` +Graph[ List[v1, v2, ...], List[edge1, edge2, ...] ] +``` + +where each edge is `DirectedEdge[u, v]` or `UndirectedEdge[u, v]`. On +construction, `Rule`/`u -> v` is accepted as shorthand for `DirectedEdge`, and +`TwoWayRule`/`u <-> v` for `UndirectedEdge`. Vertices are arbitrary +expressions. Because graphs are plain expressions, generic tools (`Part`, +`Map`, `ReplaceAll`, …) work on them, and `AdjacencyMatrix[g]` returns a dense +`List`-of-`List`s consumable directly by `Det`, `Tr`, and `Eigenvalues`. + +**MVP scope (locked):** simple graphs only — no parallel edges, no self-loops, +no edge tags, no multigraphs, no hypergraphs, and no edge/vertex weights. +`WeightedAdjacencyMatrix` and edge weights are a documented future extension. + +**Auto-display.** A bare valid `Graph[...]` result renders itself as a +node-link diagram — the REPL owns display the same way it does for `Graphics` +and `Plot` (see `src/repl.c`). In the notebook it appears as a drawn graph +rather than the `Graph[]` summary; `InputForm[g]` and +`FullForm[g]` still print the literal constructor. The auto-rendered picture +uses the default (circular) layout and styling; use `GraphPlot`/`HighlightGraph` +for explicit layout and styling control. + +## Graph +A graph value. +- `Graph[v, e]`: a graph with vertex list `v` and edge list `e`. +- `Graph[e]`: derives the vertex set from the edges, in first-appearance order + (directed by default). + +On construction the edge list is normalized and validated, producing the +canonical `Graph[List[verts], List[edges]]`: +- `u -> v` (`Rule`) and `DirectedEdge[u, v]` become `DirectedEdge[u, v]`. +- `u <-> v` (`TwoWayRule`) and `UndirectedEdge[u, v]` become + `UndirectedEdge[u, v]`. + +Malformed input is left unevaluated: self-loops, parallel/duplicate edges, +3-argument edges, or an edge endpoint absent from an explicit vertex list. +(Anti-parallel directed edges `u -> v` and `v -> u` are distinct and allowed.) + +Printing: in standard output a graph shows a terse summary, +`Graph[]`. `InputForm` and `FullForm` print the literal +constructor, which round-trips through the parser. + +``` +Graph[{1,2,3,4}, {1->2, 2->3, 3->4, 4->1}] (* Graph[<4 vertices, 4 edges>] *) +InputForm[Graph[{1,2}, {1<->2}]] (* Graph[{1, 2}, {1 <-> 2}] *) +``` + +## GraphQ +`GraphQ[g]` gives `True` if `g` is a valid graph, and `False` otherwise. A graph +is valid when it is the canonical `Graph[List, List]` with every edge a +2-argument `DirectedEdge`/`UndirectedEdge`, no self-loops, no parallel edges, +and every endpoint present in the vertex list. + +``` +GraphQ[Graph[{1,2}, {1->2}]] (* True *) +GraphQ[Graph[{1}, {1->1}]] (* False -- self-loop *) +GraphQ[5] (* False *) +``` + +## Query / representation + +All are thin readers over the canonical form and return unevaluated on a +non-graph argument. + +- `VertexList[g]` — the vertices, in canonical order. +- `EdgeList[g]` — the edges (canonical `Directed`/`UndirectedEdge` form). +- `VertexCount[g]` / `EdgeCount[g]` — cardinalities. +- `AdjacencyList[g]` — `{neighbors(v1), …}` in vertex order; + `AdjacencyList[g, v]` — neighbors of `v`. Directed edges contribute + successors (`v -> u` makes `u` a neighbor of `v`); undirected edges go both + ways. +- `IncidenceList[g, v]` — the list of edges of `g` incident to vertex `v` (both + in- and out-edges for a directed graph), in edge order; the edge counterpart of + `AdjacencyList`. `O(E)`, kinds preserved; unknown vertex → `{}`. The count + equals the undirected vertex degree. +- `VertexDegree[g]` / `VertexDegree[g, v]` — total degree (incident edges). + `VertexInDegree` / `VertexOutDegree` give in-/out-degrees: a `DirectedEdge` + adds to the source's out-degree and target's in-degree; an `UndirectedEdge` + adds to both in- and out-degree of each endpoint. +- `DirectedGraphQ[g]` — `True` iff `g` is a valid graph whose edges are all + directed. +- `EmptyGraphQ[g]` — `True` iff `g` has no edges (edgeless, on any number of + vertices). `O(1)`. +- `MixedGraphQ[g]` — `True` iff `g` has both a directed and an undirected edge; a + purely directed, purely undirected, or edgeless graph is not mixed. `O(E)`. + +``` +VertexList[Graph[{1,2,3,4},{1->2,2->3,3->4,4->1}]] (* {1, 2, 3, 4} *) +EdgeCount[Graph[{1,2,3,4},{1->2,2->3,3->4,4->1}]] (* 4 *) +VertexDegree[Graph[{1,2,3},{1<->2,2<->3}]] (* {1, 2, 1} *) +AdjacencyList[Graph[{1,2,3},{1<->2,2<->3}], 2] (* {1, 3} *) +``` + +## Matrix views (linear-algebra interop) + +- `KirchhoffMatrix[g]` — the graph Laplacian `L = D − A` (degree diagonal minus + adjacency): `L[i][i] = deg(i)`, `L[i][j] = −1` for an edge `i→j`. Every row + sums to 0. Being an ordinary matrix it feeds `Eigenvalues`/`Det` directly: the + zero-eigenvalue multiplicity is the number of connected components, and any + cofactor is the number of spanning trees (Matrix-Tree theorem). +- `AdjacencyMatrix[g]` — the dense 0/1 adjacency matrix (`n x n`, canonical + vertex order), symmetric for undirected graphs. It is an ordinary matrix, so + `Det`, `Tr`, `Eigenvalues`, etc. apply directly. +- `IncidenceMatrix[g]` — the `|V| x |E|` incidence matrix; undirected edges mark + both endpoints with `1`, directed edges are oriented (`-1` tail, `+1` head). +- `AdjacencyGraph[m]` — the inverse of `AdjacencyMatrix`: builds a graph on + vertices `1..n` from a 0/1 matrix (undirected if `m` is symmetric, else + directed). `AdjacencyGraph[AdjacencyMatrix[g]]` reproduces `g`'s edges. +- `LineGraph[g]` — the line graph: vertices are `g`'s edges, adjacent when they + share an endpoint (head-to-tail for directed graphs, which stay directed). + `L(C_n) = C_n`, `L(K_4)` has 6 vertices and 12 edges. `O(E²)`. + +``` +AdjacencyMatrix[Graph[{1,2,3,4},{1->2,2->3,3->4,4->1}]] + (* {{0,1,0,0},{0,0,1,0},{0,0,0,1},{1,0,0,0}} *) +Det[AdjacencyMatrix[Graph[{1,2,3,4},{1->2,2->3,3->4,4->1}]]] (* -1 *) +``` + +*A future `WeightedAdjacencyMatrix` would carry edge weights instead of 0/1; +not implemented in the MVP.* + +## Generators + +Each builds a canonical graph (vertices `1..n`, undirected edges) via the +constructor path: + +- `CompleteGraph[n]` — `K_n`, all `n(n-1)/2` edges. +- `CompleteGraph[{n1, n2, …}]` — the complete multipartite graph: parts of the + given sizes with an edge between every pair in different parts. + `CompleteGraph[{m, n}]` is complete bipartite `K_{m,n}`; `CompleteGraph[{2,2,2}]` + is the octahedron. +- `CycleGraph[n]` — the cycle on `1..n`. +- `PathGraph[n]` — the path `1-2-...-n`; `PathGraph[{v1,...}]` uses the given + vertices. +- `RandomGraph[{n, m}]` — a random undirected graph with `n` vertices and `m` + distinct edges (uses the seeded system RNG, so `SeedRandom` makes it + reproducible). Returns unevaluated if `m` exceeds `n(n-1)/2`. +- `StarGraph[n]` — the star `K_{1,n-1}`: a central vertex `1` joined to the + `n-1` leaves `2..n`. +- `WheelGraph[n]` — a rim cycle on `1..n-1` plus a hub `n` joined to every rim + vertex (`2(n-1)` edges; `W_4 = K_4`). Requires `n ≥ 4`. +- `GridGraph[{d1, d2, …}]` — the k-dimensional grid on a `d1 × d2 × …` lattice + (cells adjacent when they differ by 1 in one coordinate). `GridGraph[{n}]` is a + path; grids are bipartite. +- `HypercubeGraph[k]` — the k-cube `Q_k`: `2^k` vertices adjacent when they + differ in one bit (`k`-regular, bipartite; `Q_2 = C_4`). +- `TuranGraph[n, r]` — the Turán graph `T(n, r)`: the balanced complete + `r`-partite graph on `n` vertices (vertex `i` in part `i mod r`), the + `n`-vertex graph with the most edges and no `(r+1)`-clique. `O(n²)`. `T(n,1)` is + edgeless, `T(n,n) = Kₙ`, `T(4,2) = C₄`, `T(6,3)` the octahedron; its chromatic + number is `min(n, r)`. +- `CompleteKaryTree[L]` / `CompleteKaryTree[L, k]` — the complete k-ary tree with + `L` levels (`k = 2` by default): a root whose every internal node has `k` + children, filled to depth `L`. Vertices `1..V` in breadth-first order, + `V = (kᴸ−1)/(k−1)` (or `L` when `k = 1`, a path); `V−1` tree edges. Always a + tree; left unevaluated beyond a size cap. +- `CirculantGraph[n, {j1, …}]` / `CirculantGraph[n, j]` — the circulant graph on + vertices `1..n` joining `i` to `i ± jₖ (mod n)` for each jump. Vertex-transitive + and regular; `C_n({1})` is the cycle, `C_n({1,…,⌊n/2⌋})` is `Kₙ`, and a jump of + exactly `n/2` adds one matching edge per vertex. `O(n·#jumps + n²)`. +- `PrismGraph[n]` — the n-gonal prism (circular ladder): two `n`-cycles joined by + rungs `o_i ~ c_i`, i.e. `C_n □ K₂`. 3-regular, `3n` edges; `PrismGraph[4]` is the + cube. Isomorphic to `GeneralizedPetersenGraph[n, 1]`. `O(n)`, needs `n ≥ 3`. +- `AntiprismGraph[n]` — the n-antiprism: two offset `n`-cycles (outer `1..n`, + inner `n+1..2n`) with cross edges `o_i ~ c_i` and `o_i ~ c_{i+1}`. 4-regular, + `4n` edges; `AntiprismGraph[3]` is the octahedron. `O(n²)`, needs `n ≥ 3`. +- `LadderGraph[n]` — the ladder `Lₙ`: two `n`-vertex path rails joined by `n` + rungs (`= Pₙ □ P₂`); `2n` vertices, `3n−2` edges, bipartite. `L₁` is an edge, + `L₂ = C₄`. `O(n)`. +- `CocktailPartyGraph[n]` — the cocktail-party graph `K_{n×2}`: `2n` vertices in + `n` couples, each joined to all but its partner (complement of a perfect + matching). `(2n−2)`-regular with `2n(n−1)` edges. `CocktailPartyGraph[2] = C₄`, + `CocktailPartyGraph[3]` is the octahedron; equals `TuranGraph[2n, n]`. `O(n²)`. +- `KneserGraph[n, k]` — the Kneser graph `K(n, k)`: the `C(n,k)` k-subsets of + `{1..n}` (each a `List` label), adjacent iff disjoint. `K(n,1) = Kₙ`, + `K(5,2)` is the Petersen graph, `K(2k,k)` a perfect matching. Disjointness via + element bitmasks, `O(C(n,k)²)`; left unevaluated beyond a vertex cap. +- `IcosahedralGraph[]` — the icosahedron graph (a Platonic solid): a pentagonal + antiprism capped by two apexes. 12 vertices, 30 edges, 5-regular, non-bipartite, + `χ = 4`, Hamiltonian. Takes no arguments. +- `DodecahedralGraph[]` — the dodecahedron graph (a Platonic solid), which is + `GeneralizedPetersenGraph[10, 2]`: 20 vertices, 30 edges, 3-regular, girth 5, + non-bipartite, Hamiltonian, `χ = 3`. Takes no arguments. +- `GeneralizedPetersenGraph[n, k]` — `GP(n, k)`: an outer `n`-cycle `1..n`, an + inner star polygon `i_j ~ i_{j+k}` on `n+1..2n`, and spokes `o_j ~ i_j`; `2n` + vertices, 3-regular. `GP(n,1)` is the `n`-prism (`GP(4,1)` the cube), `GP(5,2)` + the Petersen graph, `GP(8,3)` the Möbius–Kantor, `GP(10,3)` the Desargues. + `O(n²)`; needs `n ≥ 3`, `1 ≤ k < n`. +- `GearGraph[n]` — the gear (cogwheel): a hub joined to alternate vertices of a + `2n`-cycle rim (a wheel with a vertex inserted between each adjacent rim pair). + `2n+1` vertices, `3n` edges, bipartite (`χ = 2`). `O(n)`. +- `HelmGraph[n]` — the helm: a wheel (hub joined to an `n`-cycle rim) with a + pendant vertex attached to each rim vertex. `2n+1` vertices, `3n` edges; the hub + has degree `n`, each rim vertex degree 4, each pendant degree 1. `O(n)`. +- `SunletGraph[n]` — the n-sunlet: a cycle `Cₙ` with one pendant vertex attached + to each cycle vertex (the corona `Cₙ ∘ K₁`). `2n` vertices, `2n` edges; cycle + vertices have degree 3, pendants degree 1. Bipartite iff `n` is even. `O(n)`. +- `FriendshipGraph[n]` — the windmill graph `Fₙ`: `n` triangles sharing one hub + vertex. `2n+1` vertices, `3n` edges; the hub has degree `2n`, every other vertex + degree `2`. `F₁` is the triangle, `F₂` the bowtie. `O(n)`. + +``` +EdgeCount[CompleteGraph[5]] (* 10 *) +EdgeList[CycleGraph[4]] (* {1<->2, 2<->3, 3<->4, 4<->1} *) +VertexDegree[PathGraph[5]] (* {1, 2, 2, 2, 1} *) +``` + +## Search & computation + +All are unweighted and build an integer-indexed adjacency on demand. + +- `FindShortestPath[g, s, t]` — a shortest path from `s` to `t` as a vertex + list (BFS; follows edge direction for directed graphs); `{}` if `t` is + unreachable. +- `GraphDistance[g, s, t]` — the length of that path; `Infinity` if unreachable. +- `ConnectedComponents[g]` / `WeaklyConnectedComponents[g]` — components of the + underlying undirected graph. +- `StronglyConnectedComponents[g]` — components following edge directions + (Tarjan). For undirected graphs this coincides with the weak components. +- `VertexOutComponent[g, v]` / `VertexInComponent[g, v]` — the vertices reachable + from `v` (over out-edges) / from which `v` is reachable (over in-edges), each + including `v`. A single BFS, `O(V+E)`; for an undirected graph both give `v`'s + connected component, while a directed path distinguishes them. An unknown vertex + leaves the call unevaluated. +- `FindSpanningTree[g]` — a spanning tree/forest as a graph (`VertexCount - 1` + edges when connected); tree edges keep their original direction. +- `TransitiveReductionGraph[g]` — the transitive reduction of a directed acyclic + graph: the fewest-edge graph on the same vertices with the same reachability + (unique for a DAG). Keeps `u→v` iff there is no length-`≥2` path `u ⇝ w ⇝ v`; + reachability comes from a BFS per vertex, `O(V·(V+E) + E·V)`. Left unevaluated + when `g` has a directed cycle (an undirected edge is a 2-cycle, so it declines + too) — the inverse operation to `TransitiveClosure`. +- `TransitiveClosure[g]` — adds an edge `u→v` whenever `v` is reachable from `u` + (directed, `O(V·(V+E))`); for an undirected graph each connected component + becomes a complete graph. +- `ConnectedGraphQ[g]` — `True` iff `g` is a single connected component. +- `BipartiteGraphQ[g]` — `True` iff the underlying undirected graph is + 2-colorable (no odd cycle). Single-BFS 2-coloring, `O(V+E)`; edge direction is + ignored and an edgeless graph is vacuously bipartite. +- `EulerianGraphQ[g]` — `True` iff `g` has an Eulerian cycle: connected (on + nonzero-degree vertices) with all even degrees for an undirected graph, or + in-degree = out-degree everywhere for a directed one. `O(V+E)`; isolated + vertices are ignored and an edgeless graph is vacuously Eulerian. +- `HamiltonianGraphQ[g]` — `True` iff `g` has a Hamiltonian cycle (a closed walk + visiting every vertex once); the predicate companion to `FindHamiltonianCycle` + and Hamiltonian counterpart of `EulerianGraphQ`. Depth-first backtracking with + degree/size prunes; direction-aware. `C_n`/`K_n`/wheels are Hamiltonian, paths + and stars are not. +- `FindEulerianCycle[g]` — an Eulerian cycle as a vertex list + `{v0, v1, …, v0}` (a closed walk using every edge exactly once), or `{}` when + none exists. Hierholzer's algorithm, `O(V+E)`: the walk is accepted only when + it consumes all edges *and* returns to its start, so a graph with an Eulerian + path but no cycle (e.g. `PathGraph[3]`) correctly yields `{}`. Works for + directed (follows out-edges) and undirected graphs; agrees with + `EulerianGraphQ` on whether a cycle exists. +- `FindHamiltonianCycle[g]` — a Hamiltonian cycle as a vertex list + `{v0, …, v0}` (a closed walk visiting every vertex exactly once), or `{}` when + none exists. Depth-first backtracking with visited-set pruning; cheap + necessary-condition prunes (fewer than 3 vertices, or any vertex without an + out- and in-neighbour) short-circuit impossible graphs. The search fixes the + start at the first vertex — WLOG, since a Hamiltonian cycle passes through + every vertex — so the result is deterministic. Follows arc direction on + directed graphs; exponential in the worst case, but instant on the small + graphs typical of a gallery. +- `FindHamiltonianPath[g]` — a Hamiltonian path as a vertex list + `{v0, …, v_{n-1}}` (a walk visiting every vertex exactly once, not required to + close), or `{}` when none exists. Depth-first backtracking; unlike a + Hamiltonian *cycle*, a path's endpoints are free, so the search is retried from + each start vertex. Follows arc direction on directed graphs; a `PathGraph` has a + Hamiltonian path but no Hamiltonian cycle. +- `ClosenessCentrality[g]` — the list of closeness centralities + `c_i = (r_i−1)² / ((n−1)·S_i)`, where `r_i` vertices are reachable from `i` at + total distance `S_i` (`(n−1)/S_i` when connected, `0` when isolated). Exact + rationals; `O(V·(V+E))` via a BFS per vertex; follows edge direction. +- `LocalClusteringCoefficient[g]` — for each vertex, the fraction of its + neighbour pairs that are themselves adjacent, `C_v = 2L_v/(d_v(d_v−1))` (and `0` + when `deg(v) < 2`). Exact rationals in vertex order; a clique gives all `1`, a + triangle-free graph (cycle, star, tree) all `0`. Edge direction is ignored + (computed on the underlying undirected graph); `O(V·d_max²)`. +- `GlobalClusteringCoefficient[g]` — the graph transitivity: three times the + number of triangles over the number of connected vertex triples, + `Σ L_v / Σ C(d_v,2)` (`0` when there are no triples). Distinct from the mean of + the local coefficients — it weights each vertex by how many triples it anchors. + Exact rational; direction ignored; `O(V·d_max²)`. +- `MeanClusteringCoefficient[g]` — the average of the local clustering + coefficients, `(1/n)Σ C_v` (every vertex counts equally, low-degree ones + contributing `0`); equals `Mean[LocalClusteringCoefficient[g]]`. Exact rational, + generally different from the global transitivity above. +- `EdgeBetweennessCentrality[g]` — for each edge, the number of shortest paths + running along it, `Σ σ_sa·σ_bt/σ_st` over pairs whose shortest path uses the + edge; the edge analogue of `BetweennessCentrality` (and the Girvan–Newman + score). Undirected edges carry paths both ways and the ordered sum is halved; + exact rationals from all-pairs BFS counts. `P₄ → {3,4,3}`, `K₄` → all `1`. +- `DegreeCentrality[g]` — for each vertex, the number of incident edges: the + ordinary degree for an undirected graph, in-degree + out-degree for a directed + one. The simplest centrality; one pass over the edges (`O(V+E)`), exact + integers in vertex order (agrees with `VertexDegree` on undirected graphs). +- `DegreeSequence[g]` — the vertex degrees (in-degree + out-degree for a directed + graph) sorted in non-increasing order; `{3,3,3,3}` for `K₄`, `{4,1,1,1,1}` for a + 4-leaf star. `O(V+E+V log V)`, exact integers — a sorted permutation of + `DegreeCentrality[g]`. +- `PageRankCentrality[g]` — the PageRank of each vertex (random surfer with + damping `d = 17/20`), as an **exact rational** probability vector summing to 1. + Rather than iterating to a float, it solves the defining linear system + `(I − dM)π = (1−d)/n·1` (with `M` the column-stochastic transition matrix, + dangling vertices teleporting uniformly) through the exact `LinearSolve`. + Follows edge direction; regular and all-dangling graphs give the uniform `1/n`, + a star's centre outranks its leaves. `O(V³)`. +- `KatzCentrality[g, α]` — the Katz centrality of each vertex with attenuation + `α` (base weight `1`): a vertex is central if pointed to by central vertices, + with a length-`k` walk discounted by `αᵏ`. Solves `(I − αAᵀ)x = 1` exactly via + `LinearSolve`, so a rational `α` gives an exact rational vector. Uses in-edges + (`Aᵀ`), so directed scores reflect who points at a vertex; `α = 0` gives all + `1`. Left unevaluated for non-numeric `α` or a singular system. `O(V³)`. +- `BetweennessCentrality[g]` — for each vertex, the number of shortest paths + through it, `Σ σ_sv·σ_vt/σ_st` (fractional when paths tie — every C₄ vertex is + `1/2`). Undirected pairs are counted once; directed keeps the ordered sum. + Exact rationals from all-pairs BFS path counts. +- `GraphDistanceMatrix[g]` — the matrix whose `(i, j)` entry is the shortest-path + distance from vertex `i` to vertex `j` (`0` on the diagonal, `Infinity` when + unreachable). One BFS per source over the direction-aware adjacency, + `O(V·(V+E))`; symmetric for undirected graphs, generally asymmetric for + directed ones. Row/column `i, j` agrees with `GraphDistance[g, i, j]`. +- `GraphReciprocity[g]` — the fraction of arcs whose reverse arc is also present, + an exact rational in `[0, 1]`: `1` for any undirected graph (every edge is + mutual), and for a directed graph the usual fraction of reciprocated edges + (`{1->2, 2->1}` → `1`, a directed cycle → `0`). `0` when there are no edges. + Modelled as a directed-arc matrix, `O(V²)`. +- `GraphAssortativity[g]` — the degree assortativity coefficient: the correlation + between the degrees of adjacent vertices, an exact rational in `[−1, 1]`. + Newman's edge-list form reduces to `(4MA − S₁²)/(2M·S_q − S₁²)` over integer + degree sums; `Indeterminate` for a regular or edgeless graph (zero variance). A + star is `−1`; direction ignored. +- `GraphDensity[g]` — the fraction of possible edges present, an exact rational + in `[0, 1]`: `2m/(n(n−1))` for an undirected graph, `m/(n(n−1))` for a directed + one (`1` for a complete graph, `0` for an empty one or fewer than two + vertices). Reduced by the evaluator, so it prints as a clean integer/`Rational`. +- `FindIndependentEdgeSet[g]` — a maximum matching: a largest set of edges no two + of which share a vertex, as a list of edges. Depth-first branch-and-bound over + the edges (take/skip) with a size bound; direction is irrelevant to + independence. `K_{2k}` and even paths/cycles give perfect/near-perfect + matchings, a star size 1. Deterministic, exponential worst case but fast on real + graphs. +- `FindClique[g]` — a largest clique (a set of pairwise-adjacent vertices) as a + list containing one vertex list (`{{1, 2, 3, 4}}` for `K₄`), or `{}` when `g` + has no vertices. A max-clique specialisation of Bron–Kerbosch: grow a clique + over candidates adjacent to all of it, pruning the branch when the remaining + candidates cannot beat the best clique found. Exponential worst case but fast + on real graphs; direction ignored; deterministic (first maximum kept). +- `FindIndependentVertexSet[g]` — a largest independent vertex set (pairwise + non-adjacent) as a list containing one vertex list, or `{}` when `g` has no + vertices. Computed as a maximum clique of the complement — the same + branch-and-bound search run over the non-adjacency. A star returns its leaves, + a complete graph a singleton; direction ignored, deterministic. +- `FindEdgeCover[g]` — a minimum edge cover: a smallest set of edges such that + every vertex is incident to at least one, as a list of edges. By Gallai it is a + maximum matching plus one incident edge per still-uncovered vertex, so + `|cover| = n − |max matching|`. Exists iff `g` has no isolated vertex (else + `{}`). A star needs all its edges; direction irrelevant. +- `FindDominatingSet[g]` — a minimum dominating set: a smallest set of vertices + such that every vertex is in it or adjacent to it, as a vertex list. Each vertex + gets a closed-neighbourhood bitmask and subsets are searched by increasing size + (first dominating one is minimum). A star is dominated by its centre, `K_n` by + any single vertex, an edgeless graph needs all its vertices. Direction ignored; + left unevaluated for large graphs. +- `FindVertexCover[g]` — a minimum vertex cover (a smallest set of vertices + touching every edge) as a flat vertex list, or `{}` when `g` has no edges. By + the Gallai identity it is the complement of a maximum independent set, so it + reuses that search and returns the vertices outside the set (`|cover| + + |independent set| = n`). A star's cover is its centre, `K_n`'s is `n−1` + vertices; direction ignored, deterministic. +- `VertexEccentricity[g, v]` — the greatest shortest-path distance from `v` to + any vertex (`Infinity` if some vertex is unreachable); `VertexEccentricity[g]` + gives the list for all vertices. +- `GraphDiameter[g]` / `GraphRadius[g]` — the max / min vertex eccentricity + (`Infinity` when not strongly connected / when no vertex reaches all others). +- `GraphCenter[g]` — the vertices whose eccentricity equals the graph radius. +- `GraphPeriphery[g]` — the vertices whose eccentricity equals the graph diameter + (the dual of `GraphCenter`); when some vertex has infinite eccentricity, the + periphery is exactly those vertices. `P₅` → `{1, 5}`, a star → its leaves, + vertex-transitive graphs → all vertices. + These derive from a BFS per vertex (`O(V·(V+E))`) and follow edge direction on + directed graphs. +- `CompleteGraphQ[g]` — `True` iff every pair of distinct vertices is adjacent + (the underlying undirected graph is `K_n`); `O(V²)`. `K_n` and a triangle are + complete, a `K_n` missing an edge is not, a graph with ≤ 1 vertex vacuously so. +- `RegularGraphQ[g]` — `True` iff every vertex has the same degree (equal + in-degrees and equal out-degrees for a directed graph); `O(V)`. A cycle is + 2-regular, `K_n` is `(n−1)`-regular, `K_{3,3}` is 3-regular, an edgeless graph + 0-regular; paths, stars, and wheels are not. A graph with ≤ 1 vertex is + vacuously regular. +- `PathGraphQ[g]` — `True` iff `g` is a path graph: a tree with maximum degree + `≤ 2` (connected, `n−1` edges, no branching or cycle). `O(V²)`; direction + ignored. A single vertex/edge and `PathGraph[n]` qualify; cycles, stars, + branches, and disconnected graphs do not. +- `TreeGraphQ[g]` — `True` iff `g` is a tree: connected with no cycles, i.e. + connected on the underlying undirected graph with exactly `n−1` distinct edges + (`n ≥ 1`). One BFS plus an edge count, `O(V²)`; a single vertex is a tree, the + empty graph and any disconnected or edgeless multi-vertex graph is not. +- `AcyclicGraphQ[g]` — `True` iff `g` has no cycle: a DAG for a directed graph, a + forest for an undirected one. `O(V+E)` (Kahn's algorithm for the directed + case, `E = V − #components` for the undirected case). +- `TopologicalSort[g]` — a vertex ordering in which every edge points forward + (Kahn's algorithm), or `$Failed` if `g` is not a directed acyclic graph + (undirected edges act as 2-cycles, so they give `$Failed`). +- `ChromaticPolynomial[g, k]` — the chromatic polynomial: the number of proper + `k`-colourings of `g`. A symbolic `k` gives the polynomial (`k(k−1)(k−2)` for a + triangle), a numeric `k` the colouring count. Computed from the Whitney + subgraph expansion `Σ_{S⊆E}(−1)^{|S|} k^{c(S)}` (integer coefficients by + component count), assembled and reduced through the evaluator so both forms + share one path. Exponential in the edge count (2^{|E|}), so it is left + unevaluated beyond a modest edge bound; direction ignored. Useful for the + chromatic number: the least `k` with `ChromaticPolynomial[g, k] > 0` (an odd + cycle gives `0` at `k = 2`, a bipartite graph a positive value). +- `FindVertexColoring[g]` — a proper colouring using the fewest colours (χ), as a + list of colour indices `1..χ`, one per vertex. Tries `k = 1, 2, …` colours and + backtracks (with a symmetry cut) to the first feasible `k`; adjacent vertices + always differ and the number of distinct colours equals `ChromaticNumber[g]`. + Direction ignored. +- `ChromaticNumber[g]` — the least number of colours for a proper colouring. + Tries `k = 1, 2, …` and tests k-colourability by backtracking (each vertex + takes a colour clashing with no coloured neighbour), with a symmetry cut + (a vertex opens at most one new colour) and early stop at the first feasible + `k`. Works for any edge count (unlike the polynomial); direction ignored. + Bipartite → `2`, odd cycle / triangle → `3`, `K_n` → `n`, edgeless → `1`. +- `FindCycle[g]` — a cycle in `g` as a list containing one cycle, that cycle + being a list of its edges (`{{1<->2, 2<->3, 3<->1}}`), or `{}` if `g` is + acyclic. DFS back-edge detection, `O(V+E)`: a directed cycle needs an on-stack + target, an undirected one a visited non-parent neighbour. Returns the first + cycle found (deterministic, not necessarily shortest); edges follow arc + direction and mirror the graph's edge kind. +- `GraphProduct[g1, g2, type]` — a product graph on the vertex pairs `V1 × V2` + (`{a, b}` labels), undirected, for `type` one of `"Cartesian"` (`a1=a2 & b1~b2` + or `b1=b2 & a1~a2`), `"Tensor"` (`a1~a2 & b1~b2`), `"Strong"` (Cartesian ∪ + Tensor), or `"Lexicographic"` (`a1~a2`, or `a1=a2 & b1~b2`). `O((n1·n2)²)`. + `P₂ □ P₂ = C₄`, `C₄ □ K₂` is the 3-regular cube, `K₂ ⊠ K₂ = K₄`. Left + unevaluated for an unknown type. +- `Subgraph[g, {v1, …}]` — the subgraph of `g` induced by the listed vertices + (in listed order, de-duplicated, restricted to vertices of `g`) together with + exactly the edges of `g` whose both endpoints are among them. Edge kinds + preserved. `Subgraph[K₄, {1,2,3}] = K₃`, `Subgraph[C₅, {1,2,3}]` is a path. +- `VertexDelete[g, v]` / `VertexDelete[g, {v1, …}]` — `g` with the given vertices + removed along with every incident edge (a `List` names several; any other + expression is a single vertex). Survivors keep their order and edge kinds. + `VertexDelete[K₄, 1] = K₃`; deleting a path's middle isolates its ends. `O(V+E)`. +- `EdgeDelete[g, e]` / `EdgeDelete[g, {e1, …}]` — `g` with the given edges removed, + keeping all vertices. An edge spec may be `DirectedEdge`/`UndirectedEdge` or the + sugar `a->b` / `a<->b`; matching is edge-kind-aware and symmetric for undirected + edges. Nonexistent edges are ignored. `O(E · #specs)`. +- `EdgeAdd[g, e]` / `EdgeAdd[g, {e1, …}]` — `g` with the given edges added, any + missing endpoint introduced as a new vertex. Same edge-spec forms as + `EdgeDelete`; self-loops and duplicate (symmetric) edges are skipped to keep the + simple-graph invariant. Adding a chord `1<->3` closes `PathGraph[3]` into a + triangle. `O((V+E)·#specs)`. +- `VertexAdd[g, v]` / `VertexAdd[g, {v1, …}]` — `g` with the given vertices added + as isolated vertices (edges unchanged); a `List` names several, any other + expression is a single vertex, and an already-present vertex is not duplicated. + New vertices are appended after the existing ones. `O((V+#new)·#new)`. +- `NeighborhoodGraph[g, v]` / `NeighborhoodGraph[g, v, k]` — the subgraph induced + by `v` and every vertex within graph distance `k` (`k = 1` by default), keeping + all edges between kept vertices. A depth-limited BFS from `v` over the + direction-aware adjacency, `O(V+E)`; `k = 0` gives just `v`. +- `GraphUnion[g1, g2]` — the graph whose vertex set is the union of the two + vertex sets and whose edge set is the union of the two edge sets, matched by + vertex identity. Vertices from `g1` keep their order, new ones from `g2` are + appended; duplicate and (for undirected) symmetric edges are collapsed, while + directed edges stay distinct from their reverse. Returns a canonical `Graph`. +- `GraphDisjointUnion[g1, g2]` — the disjoint union: vertices relabelled + `1..n1+n2` (g1's block first), the edges of both graphs (relabelled, kinds + preserved), and **no** edges between the blocks — so `g1` and `g2` are its two + components. `n1+n2` vertices, `m1+m2` edges; `GraphJoin` without the cross edges. +- `GraphJoin[g1, g2]` — the graph join: the disjoint union of `g1` and `g2` + (vertices relabelled `1..n1+n2`, `g1`'s block first) plus an undirected edge + from every `g1` vertex to every `g2` vertex. `m1 + m2 + n1·n2` edges; `K₁ ⋈ K₁` + is an edge, `P₂ ⋈ K₁` a triangle, `Kₘ ⋈ Kₙ = K₍ₘ₊ₙ₎`. Returns a canonical + `Graph`. +- `GraphIntersection[g1, g2]` — the graph with the vertices common to both and + the edges present in both (same edge-equality rules as `GraphUnion`). Identical + graphs intersect to themselves, disjoint graphs to the empty graph; returns a + canonical `Graph`. +- `GraphDifference[g1, g2]` — the graph on `g1`'s vertices with the edges of `g1` + that are not in `g2` (same edge-equality rules). `K₄ − C₄` leaves the two + diagonals; `g − g` is edgeless on `g`'s vertices; `g` minus a disjoint graph is + `g`. Returns a canonical `Graph`. +- `ReverseGraph[g]` — `g` with every directed edge reversed (`a→b` becomes `b→a`) + and undirected edges unchanged; the transpose graph. Swaps in- and out-degree, + is an involution (`ReverseGraph[ReverseGraph[g]] === g`), and is the identity on + undirected graphs. `O(V+E)`, returns a canonical `Graph`. +- `IndexGraph[g]` / `IndexGraph[g, k]` — `g` with its vertices renamed to + consecutive integers from `1` (or from `k`), in current order, edges remapped + and kinds preserved. Normalises arbitrary labels (symbols, strings, expressions) + to canonical integer indexing; `O(V+E)`, returns a canonical `Graph`. +- `GraphComplement[g]` — the graph on the same vertices whose edges are exactly + the non-edges of `g`; edgeless → complete graph, complete → edgeless, and + applying it twice restores `g`. Directed graphs stay directed (`O(V²)`). +- `VertexContract[g, {v1, v2, …}]` — merges the listed vertices into one (the + first), redirecting every incident edge to the representative, deleting the + resulting self-loops, and collapsing parallel edges. Contracting an edge's two + endpoints realises edge contraction (a triangle becomes a single edge); + contracting all vertices gives a single isolated vertex. Direction-aware, + returns a canonical `Graph`. +- `EdgeContract[g, e]` — contract edge `e`, merging its two endpoints into one + vertex, redirecting incident edges, dropping the self-loop, and collapsing + parallel edges. `e` may be `DirectedEdge`/`UndirectedEdge`, `a->b`/`a<->b`, or a + list `{a, b}`; both endpoints must be vertices. `EdgeContract[g, {u,v}]` equals + `VertexContract[g, {u,v}]`. Contracting a triangle edge leaves a single edge. +- `GraphPower[g, k]` — the k-th power: the graph on the same vertices that joins + two vertices whenever `g` has a path of length `≤ k` between them (no + self-loops). A depth-limited BFS per source over the (direction-aware) + adjacency, `O(V·(V+E))`; directed graphs stay directed. `k` must be a positive + integer, else the call is left unevaluated. `PathGraph[4]³` and `CycleGraph[5]²` + are complete; `k = 1` returns `g` unchanged. +- `VertexCoreness[g]` — for each vertex, its coreness (core number): the largest + `k` such that it lies in the k-core. Batagelj–Zaversnik peeling (remove a + minimum-degree vertex, track the running max), `O(V²)`, exact integers in vertex + order; direction ignored. A vertex has coreness `≥ k` iff it appears in + `KCoreComponents[g, k]`; the maximum coreness is the graph's degeneracy. +- `KCoreComponents[g, k]` — the connected components of the k-core of `g` (the + maximal subgraph in which every vertex has degree `≥ k`), as a list of vertex + lists. Found by repeatedly peeling any vertex whose degree drops below `k` + until stable, `O(V+E)`, then splitting the survivors into components by BFS. + Edge direction is ignored (the k-core is defined on the underlying undirected + graph); components are ordered by least vertex index. `k` must be a + non-negative integer, else the call is left unevaluated. +- `StronglyConnectedGraphQ[g]` — `True` iff every vertex is reachable from every + other following edge directions. Two BFS from vertex 0 — one over out-edges, + one over in-edges — each reaching all `n` vertices, `O(V+E)`. For an undirected + graph this coincides with `ConnectedGraphQ`; a single vertex is strongly + connected, the empty graph is not. +- `VertexConnectivity[g]` — the minimum number of vertices whose removal + disconnects `g` (`n-1` for `K_n`, `0` if already disconnected). Exact + brute-force over vertex subsets, intended for small graphs. +- `EdgeConnectivity[g]` — the minimum number of edges whose removal disconnects + `g` (`n-1` for `K_n`, `2` for a cycle, `1` for a tree/bridge, `0` if already + disconnected). Max-flow/min-cut (Edmonds–Karp, unit capacities): one source + suffices for undirected graphs, all ordered pairs for directed. + +``` +FindShortestPath[Graph[{1,2,3,4},{1->2,2->3,3->4}], 1, 4] (* {1, 2, 3, 4} *) +GraphDistance[Graph[{1,2,3,4},{1->2,2->3,3->4}], 4, 1] (* Infinity *) +StronglyConnectedComponents[Graph[{1,2,3},{1->2,2->3}]] (* {{1},{2},{3}} *) +VertexConnectivity[CycleGraph[5]] (* 2 *) +``` + +## Visualization + +`GraphPlot[g]` gives a `Graphics[...]` object drawing `g`: edges are `Line`s, +vertices are `Disk`s with a `Text` label, each preceded by an `RGBColor` +directive so the notebook Plotly serializer and the Raylib renderer both style +it with no special-casing (a window when `USE_GRAPHICS=1`, the text placeholder +otherwise). Directed edges are drawn as plain lines in the MVP (no arrowheads +yet). Because a bare `Graph` auto-displays, `GraphPlot` is only needed when you +want a non-default layout or styling. + +``` +Head[GraphPlot[CycleGraph[8]]] (* Graphics *) +Count[GraphPlot[CompleteGraph[6]], _Line, Infinity] (* 15 edges *) +``` + +### Options + +- `GraphLayout -> "name"` — vertex placement (see the layout table below). +- `VertexStyle -> color` / `EdgeStyle -> color` — a color for all vertices / + edges. Accepts `RGBColor[...]`, `GrayLevel[...]`, or a named color + (`Red`, `Blue`, `Orange`, …), which resolve to `RGBColor`. +- `VertexSize -> r` — the vertex `Disk` radius (default `0.08`); the notebook + maps radius to marker size, so larger `r` yields bigger dots. +- `VertexLabels -> None` — suppress the text labels (default draws them). + +``` +GraphPlot[CompleteGraph[8], GraphLayout -> "SpringElectricalEmbedding"] +GraphPlot[CycleGraph[10], VertexStyle -> Orange, EdgeStyle -> GrayLevel[0.7]] +GraphPlot[PathGraph[6], GraphLayout -> "LinearEmbedding", VertexLabels -> None] +``` + +### Layouts (`GraphLayout`) + +Coordinates are computed in `src/graph/layout.c` and normalized to the +`[-1, 1]` box. Every kernel is deterministic (no RNG), so notebooks reproduce +exactly. The full Wolfram-Language `GraphLayout` name list is accepted and +mapped onto the kernels below; an unrecognized name (or `None`) falls back to +circular. + +| Kernel | Wolfram names served | Notes | +|--------|----------------------|-------| +| Circular | `"CircularEmbedding"` | default; vertices on a circle | +| Spring / force-directed | `"SpringElectricalEmbedding"`, `"SpringEmbedding"`, `"TutteEmbedding"`, `"PlanarEmbedding"` | Fruchterman–Reingold: edges as springs, vertices as charges | +| Gravity | `"GravityEmbedding"` | Fruchterman–Reingold plus a central gravity well that pulls high-degree hubs inward and compacts the drawing | +| High-dimensional | `"HighDimensionalEmbedding"`, `"SpectralEmbedding"` | pivot-MDS: coordinates are BFS distances to two far-apart pivots (lays the graph along its diameter) | +| Hyperbolic | `"HyperbolicSpringEmbedding"`, `"SphericalEmbedding"` | spring layout, then a radial warp crowding vertices toward a disk boundary (Poincaré-disk feel) | +| Spiral | `"SpiralEmbedding"`, `"DiscreteSpiralEmbedding"` | Archimedean spiral; good for paths | +| Linear | `"LinearEmbedding"` | vertices on a line | +| Grid | `"GridEmbedding"` | row-major square grid | +| Random | `"RandomEmbedding"` | deterministic pseudo-random | +| Star | `"StarEmbedding"` | highest-degree vertex centered, rest on a circle | +| Radial | `"RadialEmbedding"`, `"BalloonEmbedding"`, `"HyperbolicRadialEmbedding"` | concentric BFS shells from the highest-degree root | +| Layered | `"LayeredEmbedding"`, `"LayeredDigraphEmbedding"`, `"SymmetricLayeredEmbedding"` | stacked BFS layers | +| Bipartite | `"BipartiteEmbedding"`, `"MultipartiteEmbedding"`, `"CircularMultipartiteEmbedding"` | two columns from a BFS 2-coloring (approximated for the multipartite names) | + +Edge-layout, packing, and rendering-order values (`"StraightLine"`, +`"HierarchicalEdgeBundling"`, `"LayeredTop"`, `"VertexFirst"`, …) are not +vertex layouts; they are accepted and ignored (circular fallback). + +## HighlightGraph + +`HighlightGraph[g, parts]` draws `g` with selected elements emphasized (accent +color) and everything else dimmed, returning a `Graphics` (so it auto-displays). +Each element of `parts` may be: + +- a **vertex** — highlight that vertex; +- an **edge** — `u <-> v`, `u -> v`, or `DirectedEdge`/`UndirectedEdge[u, v]` + (endpoints matched unordered); +- a **list of vertices** — treated as a *path*: its vertices and the edges + joining consecutive vertices are highlighted. + +It returns a `Graphics`, not a `Graph`: the canonical `Graph[List, List]` form +is locked to simple graphs with no annotations, so a highlight lives only in +the picture. `GraphLayout` may be given as a trailing option. + +``` +HighlightGraph[CycleGraph[6], {1, 2, 3}] (* 3 vertices *) +HighlightGraph[CompleteGraph[5], {1 <-> 2, 2 <-> 3}] (* 2 edges *) +HighlightGraph[g, {FindShortestPath[g, 1, 4]}] (* a path *) +``` + +## Graph3D + +`Graph3D[v, e]` / `Graph3D[e]` builds a graph exactly like `Graph` — same edge +sugar (`u -> v`, `u <-> v`), same simple-graph validation — but the canonical +value has head `Graph3D` and **auto-displays as a 3D node-link diagram**: a +force-directed (Fruchterman–Reingold) layout in a cube, rendered as Plotly +`scatter3d` (edges as 3D lines, vertices as markers) that you can orbit and +zoom. `Graph3D[g]` converts an existing graph to 3D, and `Graph[g3d]` converts +back to 2D. + +A `Graph3D` counts as a graph (`GraphQ` is `True`), so every query, matrix, and +algorithm builtin works on it directly. + +``` +Graph3D[{1, 2, 3, 4}, {1 <-> 2, 2 <-> 3, 3 <-> 4, 4 <-> 1}] (* 3D diagram *) +Graph3D[CompleteGraph[6]] (* wrap a graph *) +VertexCount[Graph3D[CompleteGraph[5]]] (* 5 *) +GraphQ[Graph3D[{1, 2}, {1 <-> 2}]] (* True *) +InputForm[Graph3D[{1, 2}, {1 <-> 2}]] (* Graph3D[{1, 2}, {1 <-> 2}] *) +``` + +The default 3D layout is a spring embedding; `"SphericalEmbedding"` places +vertices on a sphere. (Rendering: `src/graph/render3d.c` emits `Graphics3D`, +serialized by `graphics3d_to_plotly_json` in `src/graphics/graphics_json.c`.) diff --git a/docs/spec/changelog/2026-06-29.md b/docs/spec/changelog/2026-06-29.md index 063d572e..bffaa1e1 100644 --- a/docs/spec/changelog/2026-06-29.md +++ b/docs/spec/changelog/2026-06-29.md @@ -2,6 +2,69 @@ Feature additions and fixes recorded during this week. +## Graph subsystem: `src/graph/` — construction, queries, matrices, generators, algorithms, drawing (2026-07-04) + +New `src/graph/` module (mirrors `src/linalg/`: one builtin per translation +unit, registered in `graph.c` via `graph_init()`). Graphs are ordinary `Expr` +trees — `Graph[List[verts], List[edges]]` with each edge a `DirectedEdge[u, v]` +or `UndirectedEdge[u, v]` — so every generic tool (`Part`, `Map`, `ReplaceAll`, +pattern matching) works on them unchanged. `Rule`/`->` and `TwoWayRule`/`<->` +are accepted as parse-time sugar and normalised on construction; `<->` is a new +operator in `parse.c` at Rule precedence. The constructor validates structure +(2-arg edges, endpoints present in the vertex list, no self-loops, no parallel +edges) and returns the canonical form, leaving malformed input unevaluated. + +Builtins added, by group: + +- **Construction / predicates:** `Graph`, `GraphQ`, `DirectedGraphQ`, + `UndirectedGraphQ`, `MixedGraphQ`, `EmptyGraphQ`, `CompleteGraphQ`, + `PathGraphQ`, `TreeGraphQ`, `RegularGraphQ`, `AcyclicGraphQ`, `BipartiteQ`, + `EulerianGraphQ`, `HamiltonianGraphQ`, `ConnectedGraphQ`, + `StronglyConnectedGraphQ`, `IndexGraph`, `Subgraph`, `NeighborhoodGraph`. +- **Queries:** `VertexList`, `EdgeList`, `VertexCount`, `EdgeCount`, + `VertexDegree`, `VertexInDegree`, `VertexOutDegree`, `DegreeSequence`, + `AdjacencyList`, `IncidenceList`, `GraphDensity`, `GraphReciprocity`, + `GraphAssortativity`. +- **Matrix views:** `AdjacencyMatrix`, `IncidenceMatrix`, `KirchhoffMatrix` + (Laplacian), `DistanceMatrix`. +- **Structural ops:** `GraphComplement`, `GraphUnion`, `GraphDisjointUnion`, + `GraphIntersection`, `GraphDifference`, `GraphJoin`, `GraphProduct`, + `GraphPower`, `ReverseGraph`, `LineGraph`, `TransitiveClosureGraph`, + `TransitiveReductionGraph`, `VertexAdd`/`VertexDelete`/`VertexContract`, + `EdgeAdd`/`EdgeDelete`/`EdgeContract`. +- **Generators:** `CompleteGraph`, `CompleteKaryTree`, `CycleGraph`, + `PathGraph`, `CirculantGraph`, `CocktailPartyGraph`, `TuranGraph`, + `KneserGraph`, `GeneralizedPetersenGraph`, `PrismGraph`, `AntiprismGraph`, + `LadderGraph`, `GearGraph`, `HelmGraph`, `SunletGraph`, `FriendshipGraph`, + `WheelGraph`, dodecahedral/icosahedral, and `RandomGraph`. +- **Algorithms:** `FindShortestPath`, `GraphDistance`, `ConnectedComponents`, + `WeaklyConnectedComponents`, `ConnectedComponentCount`, `FindSpanningTree`, + `VertexConnectivity`/`EdgeConnectivity`, `KCoreComponents`, `VertexCoreness`, + `FindEulerianCycle`, `FindHamiltonianCycle`/`FindHamiltonianPath`, + `FindClique`, `FindIndependentVertexSet`, `FindVertexCover`, `FindEdgeCover`, + `FindDominatingSet`, `FindGraphMatching`, `FindVertexColoring`, + `ChromaticNumber`, `ChromaticPolynomial`. +- **Centrality:** `PageRank`, `KatzCentrality`, `BetweennessCentrality`, + `EdgeBetweennessCentrality`, `ClosenessCentrality`, `DegreeCentrality`, + `LocalClusteringCoefficient`, `MeanClusteringCoefficient`, + `GlobalClusteringCoefficient`. +- **Drawing:** `GraphPlot` (2D node-link diagram → `Graphics[...]`), + `HighlightGraph`, `Graph3D`/`GraphPlot3D` (→ `Graphics3D[...]`). A bare valid + `Graph`/`Graph3D` result auto-renders as a diagram in the REPL and notebook + sidecar, the same way `Graphics` does. + +`PageRank` and `KatzCentrality` are computed **exactly** by assembling a +rational transition/resolvent matrix and calling the existing `LinearSolve`, +so results are exact rationals rather than floating-point approximations. +Shared helpers (adjacency build, validity checks, vertex indexing, layout, +Graphics/Graphics3D emission) live in `graph_util.c`, `layout.c`, `render3d.c`. +Graph-diagram serialisation was added to `graphics_json.c` +(`graphics3d_to_plotly_json`, diagram mode for `Disk`/`Point`/`Text`). +`print.c` renders edges infix (`u -> v` / `u <-> v`) and prints a bare `Graph` +as a terse `Graph[]` summary (InputForm/FullForm still +round-trip through the literal constructor). Every builtin is `Protected`, has +a docstring, and is covered by `tests/test_graph.c`. + ## RootReduce: WL-faithful algebraic-number canonicalisation (G1–G5) (2026-07-04) Brought `RootReduce` up to the core Wolfram-Language contract. Previously it only diff --git a/makefile b/makefile index 134554a3..03b6bbdc 100644 --- a/makefile +++ b/makefile @@ -15,7 +15,7 @@ else endif CC = gcc -CFLAGS = -O3 -std=c99 -Wall -Wextra -g -I./src -I./src/list -I./src/linalg -I./src/numbertheory -I./src/poly -I./src/simp -I./src/calculus -I./src/sum -I./src/product -I./src/special_functions -I./src/numerical_calculus -I./src/numerical_roots -I./src/graphics -I/usr/local/include +CFLAGS = -O3 -std=c99 -Wall -Wextra -g -I./src -I./src/list -I./src/linalg -I./src/numbertheory -I./src/poly -I./src/simp -I./src/calculus -I./src/sum -I./src/product -I./src/special_functions -I./src/numerical_calculus -I./src/numerical_roots -I./src/graphics -I./src/graph -I/usr/local/include # Readline is available on macOS and Linux but not on Windows (MinGW). # Build with USE_READLINE=0 to disable it explicitly (e.g. for cross-builds @@ -146,7 +146,7 @@ ifeq ($(USE_FLINT), 1) endif SRC_DIR = src -SRC = $(wildcard $(SRC_DIR)/*.c) $(wildcard $(SRC_DIR)/list/*.c) $(wildcard $(SRC_DIR)/linalg/*.c) $(wildcard $(SRC_DIR)/numbertheory/*.c) $(wildcard $(SRC_DIR)/poly/*.c) $(wildcard $(SRC_DIR)/simp/*.c) $(wildcard $(SRC_DIR)/calculus/*.c) $(wildcard $(SRC_DIR)/sum/*.c) $(wildcard $(SRC_DIR)/product/*.c) $(wildcard $(SRC_DIR)/special_functions/*.c) $(wildcard $(SRC_DIR)/numerical_calculus/*.c) $(wildcard $(SRC_DIR)/numerical_roots/*.c) $(wildcard $(SRC_DIR)/graphics/*.c) +SRC = $(wildcard $(SRC_DIR)/*.c) $(wildcard $(SRC_DIR)/list/*.c) $(wildcard $(SRC_DIR)/linalg/*.c) $(wildcard $(SRC_DIR)/numbertheory/*.c) $(wildcard $(SRC_DIR)/poly/*.c) $(wildcard $(SRC_DIR)/simp/*.c) $(wildcard $(SRC_DIR)/calculus/*.c) $(wildcard $(SRC_DIR)/sum/*.c) $(wildcard $(SRC_DIR)/product/*.c) $(wildcard $(SRC_DIR)/special_functions/*.c) $(wildcard $(SRC_DIR)/numerical_calculus/*.c) $(wildcard $(SRC_DIR)/numerical_roots/*.c) $(wildcard $(SRC_DIR)/graphics/*.c) $(wildcard $(SRC_DIR)/graph/*.c) ifneq ($(USE_GRAPHICS), 1) SRC := $(filter-out $(SRC_DIR)/graphics/render.c $(SRC_DIR)/graphics/hershey_font.c, $(SRC)) endif diff --git a/src/core.c b/src/core.c index ec5b1a22..faf55026 100644 --- a/src/core.c +++ b/src/core.c @@ -654,6 +654,8 @@ void core_init(void) { zero_test_init(); void graphics_init(void); graphics_init(); + void graph_init(void); + graph_init(); /* Options/SetOptions/OptionValue + the default-options registry. Runs last * so every option-name symbol used by the registry is already interned. */ diff --git a/src/graph/acyclic.c b/src/graph/acyclic.c new file mode 100644 index 00000000..4c9fed25 --- /dev/null +++ b/src/graph/acyclic.c @@ -0,0 +1,100 @@ +/* acyclic.c - AcyclicGraphQ[g] and TopologicalSort[g]. + * + * Both rest on Kahn's algorithm (topological sort by repeatedly removing + * in-degree-0 vertices): a directed graph is acyclic iff every vertex is + * eventually removed. O(V+E) over the shared integer adjacency. + * + * TopologicalSort[g] -> a vertex order with every edge pointing forward, or + * $Failed if g has a directed cycle (or undirected + * edges, which act as 2-cycles). Directed graphs only, + * matching the Wolfram Language. + * AcyclicGraphQ[g] -> True iff g has no cycle. For an all-directed graph + * that means a DAG (Kahn removes all vertices); for a + * graph with undirected edges it means the underlying + * undirected graph is a forest (|E| = |V| - #components). + * + * Memory (SPEC section 4): returns freshly-allocated results; frees res. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +/* Kahn's algorithm. Returns a malloc'd array of vertex indices in topological + * order (caller frees) and writes the number ordered to *count. When *count + * equals a->n the graph is a DAG; otherwise a cycle blocked the remainder. */ +static int* topo_order(const GraphAdj* a, int* count) { + int n = a->n; + int* indeg = malloc((size_t)(n > 0 ? n : 1) * sizeof(int)); + int* order = malloc((size_t)(n > 0 ? n : 1) * sizeof(int)); + int* q = malloc((size_t)(n > 0 ? n : 1) * sizeof(int)); + if (!indeg || !order || !q) { free(indeg); free(order); free(q); *count = -1; return NULL; } + for (int i = 0; i < n; i++) indeg[i] = a->indeg[i]; + int head = 0, tail = 0; + for (int i = 0; i < n; i++) if (indeg[i] == 0) q[tail++] = i; + int k = 0; + while (head < tail) { + int u = q[head++]; + order[k++] = u; + for (int j = 0; j < a->outdeg[u]; j++) { + int w = a->out[u][j]; + if (--indeg[w] == 0) q[tail++] = w; + } + } + free(indeg); free(q); + *count = k; + return order; +} + +Expr* builtin_topological_sort(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + GraphAdj* a = graph_build_adj(res->data.function.args[0]); + if (!a) return NULL; + int cnt = 0; + int* order = topo_order(a, &cnt); + if (!order) { graph_adj_free(a); return NULL; } + + Expr* out; + if (cnt == a->n) { + Expr** items = (a->n > 0) ? calloc((size_t)a->n, sizeof(Expr*)) : NULL; + for (int i = 0; i < a->n; i++) + items[i] = expr_copy(a->verts->data.function.args[order[i]]); + out = expr_new_function(expr_new_symbol(SYM_List), items, (size_t)a->n); + free(items); + } else { + out = expr_new_symbol(SYM_DollarFailed); /* cyclic / undirected */ + } + free(order); graph_adj_free(a); + return out; +} + +Expr* builtin_acyclic_graph_q(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return expr_new_symbol(SYM_False); + + const Expr* edges = g->data.function.args[1]; + size_t ne = edges->data.function.arg_count; + int all_directed = 1; + for (size_t i = 0; i < ne; i++) + if (graph_edge_kind(edges->data.function.args[i]) != SYM_DirectedEdge) { all_directed = 0; break; } + + GraphAdj* a = graph_build_adj(g); + if (!a) return NULL; + + int acyclic; + if (all_directed) { + int cnt = 0; + int* order = topo_order(a, &cnt); + if (!order) { graph_adj_free(a); return NULL; } + acyclic = (cnt == a->n); /* DAG iff every vertex removed */ + free(order); + } else { + /* Underlying undirected graph is a forest iff E = V - components. */ + int comps = graph_count_components(a, NULL, NULL); + acyclic = ((int)ne == a->n - comps); + } + graph_adj_free(a); + return expr_new_symbol(acyclic ? SYM_True : SYM_False); +} diff --git a/src/graph/adjgraph.c b/src/graph/adjgraph.c new file mode 100644 index 00000000..050129e8 --- /dev/null +++ b/src/graph/adjgraph.c @@ -0,0 +1,76 @@ +/* adjgraph.c - AdjacencyGraph[m]: build a graph from a 0/1 adjacency matrix. + * + * The inverse of AdjacencyMatrix. Vertices are the integers 1..n. A symmetric + * matrix yields an undirected graph (one UndirectedEdge per i + +/* Integer value of matrix entry, or -1 if not a plain integer. */ +static int entry_int(const Expr* row, size_t j) { + const Expr* e = row->data.function.args[j]; + if (e->type != EXPR_INTEGER) return -1; + return (int)e->data.integer; +} + +Expr* builtin_adjacency_graph(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* m = res->data.function.args[0]; + if (!graph_is_list(m)) return NULL; + + size_t n = m->data.function.arg_count; + /* Validate: n x n, entries 0/1. */ + for (size_t i = 0; i < n; i++) { + const Expr* row = m->data.function.args[i]; + if (!graph_is_list(row) || row->data.function.arg_count != n) return NULL; + for (size_t j = 0; j < n; j++) { + int v = entry_int(row, j); + if (v != 0 && v != 1) return NULL; + } + } + + /* Symmetry test. */ + int symmetric = 1; + for (size_t i = 0; i < n && symmetric; i++) + for (size_t j = 0; j < n; j++) + if (entry_int(m->data.function.args[i], j) + != entry_int(m->data.function.args[j], i)) { symmetric = 0; break; } + + /* Vertices 1..n. */ + Expr** verts = (n > 0) ? calloc(n, sizeof(Expr*)) : NULL; + for (size_t i = 0; i < n; i++) verts[i] = expr_new_integer((int64_t)i + 1); + Expr* vlist = expr_new_function(expr_new_symbol(SYM_List), verts, n); + free(verts); + + /* Edges. Upper-bound count then trim via arg_count. */ + Expr** edges = (n > 0) ? calloc(n * n, sizeof(Expr*)) : NULL; + size_t ne = 0; + for (size_t i = 0; i < n; i++) { + for (size_t j = 0; j < n; j++) { + if (i == j) continue; /* ignore self-loops */ + if (entry_int(m->data.function.args[i], j) != 1) continue; + if (symmetric && j < i) continue; /* one undirected edge per pair */ + Expr* ea[2] = { expr_new_integer((int64_t)i + 1), + expr_new_integer((int64_t)j + 1) }; + edges[ne++] = expr_new_function( + expr_new_symbol(symmetric ? SYM_UndirectedEdge : SYM_DirectedEdge), + ea, 2); + } + } + Expr* elist = expr_new_function(expr_new_symbol(SYM_List), edges, ne); + free(edges); + + Expr* gargs[2] = { vlist, elist }; + return expr_new_function(expr_new_symbol(SYM_Graph), gargs, 2); +} diff --git a/src/graph/adjlist.c b/src/graph/adjlist.c new file mode 100644 index 00000000..7238e979 --- /dev/null +++ b/src/graph/adjlist.c @@ -0,0 +1,71 @@ +/* adjlist.c - AdjacencyList[g] and AdjacencyList[g, v]. + * + * Neighbors of v: successors for directed edges (v -> u yields u), and both + * endpoints for undirected edges. This matches the row convention of + * AdjacencyMatrix (a 1 in row v, column u means v is adjacent to u). Neighbors + * are returned in first-appearance order, de-duplicated. + * + * AdjacencyList[g] -> {neighbors(v1), neighbors(v2), ...} in vertex order. + * AdjacencyList[g, v] -> neighbors(v). + * + * Memory (SPEC section 4): returns freshly-allocated lists; evaluator frees res. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +/* Build List of neighbors of `v` (deduplicated, first-appearance order). */ +static Expr* neighbors_of(const Expr* g, const Expr* v) { + const Expr* edges = g->data.function.args[1]; + size_t ne = edges->data.function.arg_count; + Expr** nbr = (ne > 0) ? calloc(ne * 2, sizeof(Expr*)) : NULL; + size_t n = 0; + + for (size_t i = 0; i < ne; i++) { + const Expr* e = edges->data.function.args[i]; + const char* kind = graph_edge_kind(e); + const Expr* a = e->data.function.args[0]; + const Expr* b = e->data.function.args[1]; + const Expr* add = NULL; + if (kind == SYM_UndirectedEdge) { + if (expr_eq(a, v)) add = b; + else if (expr_eq(b, v)) add = a; + } else { + if (expr_eq(a, v)) add = b; /* successor only */ + } + if (add) { + int seen = 0; + for (size_t j = 0; j < n; j++) + if (expr_eq(nbr[j], add)) { seen = 1; break; } + if (!seen) nbr[n++] = expr_copy((Expr*)add); + } + } + Expr* list = expr_new_function(expr_new_symbol(SYM_List), nbr, n); + free(nbr); + return list; +} + +Expr* builtin_adjacency_list(Expr* res) { + size_t argc = res->data.function.arg_count; + if (argc < 1 || argc > 2) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + const Expr* verts = g->data.function.args[0]; + + if (argc == 2) { + const Expr* v = res->data.function.args[1]; + if (graph_vertex_index(verts, v) < 0) return NULL; + return neighbors_of(g, v); + } + + size_t nv = verts->data.function.arg_count; + Expr** rows = (nv > 0) ? calloc(nv, sizeof(Expr*)) : NULL; + if (nv > 0 && !rows) return NULL; + for (size_t i = 0; i < nv; i++) + rows[i] = neighbors_of(g, verts->data.function.args[i]); + Expr* list = expr_new_function(expr_new_symbol(SYM_List), rows, nv); + free(rows); + return list; +} diff --git a/src/graph/adjmat.c b/src/graph/adjmat.c new file mode 100644 index 00000000..f9d950d2 --- /dev/null +++ b/src/graph/adjmat.c @@ -0,0 +1,53 @@ +/* adjmat.c - AdjacencyMatrix[g]: dense 0/1 adjacency matrix. + * + * Returns an n x n dense List-of-Lists (n = |V|, in canonical vertex order), + * consumable directly by Det, Tr, and Eigenvalues with no linalg changes. + * A DirectedEdge[a,b] sets M[a][b] = 1; an UndirectedEdge sets both M[a][b] and + * M[b][a], so undirected graphs yield a symmetric matrix. Entries are always + * 0/1 since parallel edges are forbidden. + * + * Future hook: a WeightedAdjacencyMatrix would fill entries with edge weights + * instead of 1 (Locked Decision 2); not implemented in the MVP. + * + * Memory (SPEC section 4): returns a freshly-allocated matrix; frees res. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +Expr* builtin_adjacency_matrix(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + size_t n = verts->data.function.arg_count; + + int* grid = (n > 0) ? calloc(n * n, sizeof(int)) : NULL; + if (n > 0 && !grid) return NULL; + + for (size_t k = 0; k < edges->data.function.arg_count; k++) { + const Expr* e = edges->data.function.args[k]; + const char* kind = graph_edge_kind(e); + int ia = graph_vertex_index(verts, e->data.function.args[0]); + int ib = graph_vertex_index(verts, e->data.function.args[1]); + grid[(size_t)ia * n + (size_t)ib] = 1; + if (kind == SYM_UndirectedEdge) grid[(size_t)ib * n + (size_t)ia] = 1; + } + + Expr** rows = (n > 0) ? calloc(n, sizeof(Expr*)) : NULL; + for (size_t i = 0; i < n; i++) { + Expr** row = calloc(n, sizeof(Expr*)); + for (size_t j = 0; j < n; j++) + row[j] = expr_new_integer(grid[i * n + j]); + rows[i] = expr_new_function(expr_new_symbol(SYM_List), row, n); + free(row); + } + Expr* mat = expr_new_function(expr_new_symbol(SYM_List), rows, n); + free(rows); + free(grid); + return mat; +} diff --git a/src/graph/antiprismgraph.c b/src/graph/antiprismgraph.c new file mode 100644 index 00000000..edef5fea --- /dev/null +++ b/src/graph/antiprismgraph.c @@ -0,0 +1,57 @@ +/* antiprismgraph.c - AntiprismGraph[n]: the n-antiprism, two concentric n-cycles + * (outer 1..n, inner n+1..2n) whose rings are offset so each outer vertex joins + * two consecutive inner vertices. + * + * Edges: the outer cycle o_i ~ o_{i+1}, the inner cycle c_i ~ c_{i+1}, and the + * cross edges o_i ~ c_i and o_i ~ c_{i+1} (indices mod n). Every vertex has + * degree 4 (two ring + two cross), giving a 4-regular graph with 4n edges. O(n^2) + * via a boolean adjacency. AntiprismGraph[3] is the octahedron. + * + * n >= 3 must be an integer. Memory (SPEC section 4): returns a freshly-built + * Graph; frees res. NULL otherwise. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +Expr* builtin_antiprism_graph(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* ne = res->data.function.args[0]; + if (ne->type != EXPR_INTEGER) return NULL; + long n = (long)ne->data.integer; + if (n < 3) return NULL; + + long V = 2 * n; + char* adj = calloc((size_t)V * (size_t)V, 1); + #define SET(a,b) do { adj[(size_t)(a) * V + (b)] = adj[(size_t)(b) * V + (a)] = 1; } while (0) + for (long i = 0; i < n; i++) { + long o = i, c = n + i, o1 = (i + 1) % n, c1 = n + (i + 1) % n; + SET(o, o1); /* outer cycle */ + SET(c, c1); /* inner cycle */ + SET(o, c); /* cross */ + SET(o, c1); /* offset cross */ + } + #undef SET + + Expr** vc = malloc((size_t)V * sizeof(Expr*)); + for (long i = 0; i < V; i++) vc[i] = expr_new_integer(i + 1); + + size_t cap = (size_t)V * (size_t)(V - 1) / 2; + Expr** ec = malloc(cap * sizeof(Expr*)); + size_t me = 0; + for (long i = 0; i < V; i++) + for (long j = i + 1; j < V; j++) + if (adj[(size_t)i * V + j]) { + Expr* a[2] = { expr_new_integer(i + 1), expr_new_integer(j + 1) }; + ec[me++] = expr_new_function(expr_new_symbol(SYM_UndirectedEdge), a, 2); + } + free(adj); + + Expr* vlist = expr_new_function(expr_new_symbol(SYM_List), vc, (size_t)V); + Expr* elist = expr_new_function(expr_new_symbol(SYM_List), ec, me); + free(vc); free(ec); + Expr* gargs[2] = { vlist, elist }; + return expr_new_function(expr_new_symbol(SYM_Graph), gargs, 2); +} diff --git a/src/graph/betweenness.c b/src/graph/betweenness.c new file mode 100644 index 00000000..d1eb3e00 --- /dev/null +++ b/src/graph/betweenness.c @@ -0,0 +1,100 @@ +/* betweenness.c - BetweennessCentrality[g]. + * + * For a vertex v, the betweenness is the sum over ordered pairs (s,t), both != v, + * of the fraction of shortest s-t paths that pass through v: + * c(v) = sum_{s != v != t} sigma_sv * sigma_vt / sigma_st , over pairs with + * d(s,v) + d(v,t) = d(s,t), + * where sigma_ab counts shortest a->b paths. Undirected graphs count each + * unordered pair once (the ordered sum is halved), matching the Wolfram Language; + * directed graphs keep the ordered sum. Distances follow edge direction. + * + * All-pairs distances and path counts come from one BFS per source (Brandes-style + * counting), then an O(V^3) accumulation. Values are exact: each term is + * num/sigma_st and the per-vertex sum is reduced by the evaluator, so a graph + * with tied shortest paths yields clean fractions (every C4 vertex is 1/2). + * + * Memory (SPEC section 4): returns a fresh List; frees res. NULL on a non-graph. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include "eval.h" +#include + +/* BFS from s over out[]: fills d[] (-1 unreachable) and sig[] (# shortest paths). */ +static void bfs_paths(const GraphAdj* a, int s, int* d, long* sig, int* q) { + int n = a->n; + for (int i = 0; i < n; i++) { d[i] = -1; sig[i] = 0; } + int head = 0, tail = 0; + d[s] = 0; sig[s] = 1; q[tail++] = s; + while (head < tail) { + int u = q[head++]; + for (int e = 0; e < a->outdeg[u]; e++) { + int w = a->out[u][e]; + if (d[w] < 0) { d[w] = d[u] + 1; q[tail++] = w; } + if (d[w] == d[u] + 1) sig[w] += sig[u]; + } + } +} + +Expr* builtin_betweenness_centrality(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + const Expr* edges = g->data.function.args[1]; + size_t ne = edges->data.function.arg_count; + int directed = (ne > 0); + for (size_t i = 0; i < ne; i++) + if (graph_edge_kind(edges->data.function.args[i]) != SYM_DirectedEdge) { directed = 0; break; } + + GraphAdj* a = graph_build_adj(g); + if (!a) return NULL; + int n = a->n; + + int* D = (n > 0) ? malloc((size_t)n * n * sizeof(int)) : NULL; + long* SIG = (n > 0) ? malloc((size_t)n * n * sizeof(long)) : NULL; + int* q = (n > 0) ? malloc((size_t)n * sizeof(int)) : NULL; + if (n > 0 && (!D || !SIG || !q)) { free(D); free(SIG); free(q); graph_adj_free(a); return NULL; } + for (int s = 0; s < n; s++) bfs_paths(a, s, D + (size_t)s * n, SIG + (size_t)s * n, q); + free(q); + + Expr** items = (n > 0) ? calloc((size_t)n, sizeof(Expr*)) : NULL; + for (int v = 0; v < n; v++) { + /* Collect the nonzero terms sigma_sv * sigma_vt / sigma_st. */ + Expr** terms = (n > 1) ? calloc((size_t)n * n, sizeof(Expr*)) : NULL; + size_t k = 0; + for (int s = 0; s < n; s++) { + if (s == v) continue; + long dsv = D[(size_t)s * n + v], ssv = SIG[(size_t)s * n + v]; + if (dsv < 0) continue; + for (int t = 0; t < n; t++) { + if (t == v || t == s) continue; + long dvt = D[(size_t)v * n + t], dst = D[(size_t)s * n + t]; + if (dvt < 0 || dst < 0 || dsv + dvt != dst) continue; + long num = ssv * SIG[(size_t)v * n + t]; /* sigma_sv * sigma_vt */ + long den = SIG[(size_t)s * n + t]; /* sigma_st */ + if (num == 0 || den == 0) continue; + Expr* pa[2] = { expr_new_integer(den), expr_new_integer(-1) }; + Expr* inv = expr_new_function(expr_new_symbol(SYM_Power), pa, 2); + Expr* ta[2] = { expr_new_integer(num), inv }; + terms[k++] = expr_new_function(expr_new_symbol(SYM_Times), ta, 2); + } + } + Expr* sum = (k == 0) ? expr_new_integer(0) + : expr_new_function(expr_new_symbol(SYM_Plus), terms, k); + free(terms); + if (!directed && k > 0) { /* halve the ordered-pair sum */ + Expr* ha[2] = { expr_new_integer(2), expr_new_integer(-1) }; + Expr* half = expr_new_function(expr_new_symbol(SYM_Power), ha, 2); + Expr* ma[2] = { sum, half }; + sum = expr_new_function(expr_new_symbol(SYM_Times), ma, 2); + } + items[v] = evaluate(sum); + } + free(D); free(SIG); + Expr* out = expr_new_function(expr_new_symbol(SYM_List), items, (size_t)n); + free(items); + graph_adj_free(a); + return out; +} diff --git a/src/graph/bipartiteq.c b/src/graph/bipartiteq.c new file mode 100644 index 00000000..1cc706eb --- /dev/null +++ b/src/graph/bipartiteq.c @@ -0,0 +1,58 @@ +/* bipartiteq.c - BipartiteGraphQ[g]: True iff the underlying undirected graph + * is 2-colorable (no odd cycle), i.e. its vertices split into two independent + * sets. Edge direction is ignored (a graph is bipartite as an undirected + * structure). Vacuously True for an edgeless graph. + * + * Algorithm: a single BFS per component, 2-coloring as it goes; a bipartite + * graph is exactly one with no edge joining two same-colored vertices. O(V+E) + * over the on-demand integer adjacency (graph_build_adj), which stores each + * undirected edge symmetrically in out[]/in[], so scanning both neighbor lists + * visits the whole undirected neighborhood. + * + * Memory (SPEC section 4): returns a fresh True/False symbol; the evaluator + * frees res. False (not NULL) on a non-graph argument, matching GraphQ. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +Expr* builtin_bipartite_graph_q(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return expr_new_symbol(SYM_False); + + GraphAdj* a = graph_build_adj(g); + if (!a) return expr_new_symbol(SYM_False); + int n = a->n; + + int bipartite = 1; + int* color = (n > 0) ? malloc((size_t)n * sizeof(int)) : NULL; + int* queue = (n > 0) ? malloc((size_t)n * sizeof(int)) : NULL; + if (n > 0 && (!color || !queue)) { free(color); free(queue); graph_adj_free(a); return NULL; } + for (int i = 0; i < n; i++) color[i] = -1; + + for (int s = 0; s < n && bipartite; s++) { + if (color[s] >= 0) continue; + int head = 0, tail = 0; + color[s] = 0; queue[tail++] = s; + while (head < tail && bipartite) { + int u = queue[head++]; + /* Visit both stored directions to cover the undirected neighborhood. */ + for (int pass = 0; pass < 2 && bipartite; pass++) { + int deg = pass ? a->indeg[u] : a->outdeg[u]; + int* nb = pass ? a->in[u] : a->out[u]; + for (int e = 0; e < deg; e++) { + int v = nb[e]; + if (color[v] < 0) { color[v] = 1 - color[u]; queue[tail++] = v; } + else if (color[v] == color[u]) { bipartite = 0; break; } + } + } + } + } + + free(color); free(queue); + graph_adj_free(a); + return expr_new_symbol(bipartite ? SYM_True : SYM_False); +} diff --git a/src/graph/centrality.c b/src/graph/centrality.c new file mode 100644 index 00000000..59e2d21e --- /dev/null +++ b/src/graph/centrality.c @@ -0,0 +1,70 @@ +/* centrality.c - ClosenessCentrality[g]. + * + * For each vertex i let r_i be the number of vertices reachable from i (itself + * included) and S_i the sum of shortest-path distances from i to those reachable + * vertices. Closeness centrality (Wolfram's disconnected-safe form) is + * c_i = (r_i - 1)^2 / ((n - 1) * S_i), + * which for a connected graph reduces to the usual (n - 1) / S_i, and is 0 for an + * isolated vertex. Distances follow edge direction on a directed graph. + * + * The result is exact: each c_i is built as num/den and reduced by the evaluator, + * so a symmetric graph yields clean rationals (e.g. every cycle vertex is 3/4 on + * C4). O(V*(V+E)) — a BFS per vertex over the shared adjacency. + * + * Memory (SPEC section 4): returns a fresh List; frees res. NULL on a non-graph. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include "eval.h" +#include + +/* BFS from src over out[]; dist[i] = distance or -1 if unreachable. */ +static void bfs_out(const GraphAdj* a, int src, int* dist, int* q) { + int n = a->n; + for (int i = 0; i < n; i++) dist[i] = -1; + int head = 0, tail = 0; + dist[src] = 0; q[tail++] = src; + while (head < tail) { + int u = q[head++]; + for (int j = 0; j < a->outdeg[u]; j++) { + int w = a->out[u][j]; + if (dist[w] < 0) { dist[w] = dist[u] + 1; q[tail++] = w; } + } + } +} + +/* Exact num/den as a reduced value (integer or Rational). den > 0 required. */ +static Expr* ratio(long num, long den) { + Expr* pa[2] = { expr_new_integer(den), expr_new_integer(-1) }; + Expr* inv = expr_new_function(expr_new_symbol(SYM_Power), pa, 2); + Expr* ta[2] = { expr_new_integer(num), inv }; + return evaluate(expr_new_function(expr_new_symbol(SYM_Times), ta, 2)); +} + +Expr* builtin_closeness_centrality(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + GraphAdj* a = graph_build_adj(res->data.function.args[0]); + if (!a) return NULL; + int n = a->n; + + int* dist = malloc((size_t)(n > 0 ? n : 1) * sizeof(int)); + int* q = malloc((size_t)(n > 0 ? n : 1) * sizeof(int)); + Expr** items = (n > 0) ? calloc((size_t)n, sizeof(Expr*)) : NULL; + if ((n > 0 && (!dist || !q || !items))) { free(dist); free(q); free(items); graph_adj_free(a); return NULL; } + + for (int i = 0; i < n; i++) { + bfs_out(a, i, dist, q); + long reach = 0, sum = 0; + for (int j = 0; j < n; j++) if (dist[j] >= 0) { reach++; sum += dist[j]; } + long den = (long)(n - 1) * sum; /* (n-1) * S_i */ + items[i] = (den <= 0) ? expr_new_integer(0) + : ratio((reach - 1) * (reach - 1), den); + } + free(dist); free(q); + Expr* out = expr_new_function(expr_new_symbol(SYM_List), items, (size_t)n); + free(items); + graph_adj_free(a); + return out; +} diff --git a/src/graph/chromaticnumber.c b/src/graph/chromaticnumber.c new file mode 100644 index 00000000..172093f0 --- /dev/null +++ b/src/graph/chromaticnumber.c @@ -0,0 +1,76 @@ +/* chromaticnumber.c - ChromaticNumber[g]: the least number of colours needed to + * properly colour g (adjacent vertices differ), i.e. the smallest k for which a + * proper k-colouring exists. + * + * Rather than reading it off the chromatic polynomial (exponential in edges), + * this tries k = 1, 2, ... and tests k-colourability directly by backtracking: + * colour vertices in order, giving each a colour in 1..k that clashes with no + * already-coloured neighbour. Two symmetry cuts keep it fast: a vertex may open + * at most one brand-new colour (colour <= maxUsed + 1), and the first k that + * succeeds is the chromatic number, so the search stops immediately. Edge + * direction is ignored (colouring is an undirected notion). + * + * Conventions: an empty graph (no vertices) has chromatic number 0; a graph with + * vertices but no edges needs 1 colour; a bipartite graph 2; K_n needs n. + * + * Memory (SPEC section 4): returns a fresh integer; frees res. NULL on a + * non-graph argument. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +typedef struct { const char* adj; int n; int k; int* color; } ColorEnv; + +/* Try to colour vertices v..n-1; maxUsed = highest colour used in 0..v-1. */ +static int color_from(ColorEnv* e, int v, int maxUsed) { + if (v == e->n) return 1; + int limit = (maxUsed + 1 < e->k) ? maxUsed + 1 : e->k; /* symmetry cut */ + for (int c = 1; c <= limit; c++) { + int ok = 1; + for (int u = 0; u < e->n; u++) + if (e->adj[(size_t)v * e->n + u] && e->color[u] == c) { ok = 0; break; } + if (!ok) continue; + e->color[v] = c; + if (color_from(e, v + 1, (c > maxUsed) ? c : maxUsed)) return 1; + e->color[v] = 0; + } + return 0; +} + +Expr* builtin_chromatic_number(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + int n = (int)verts->data.function.arg_count; + size_t ne = edges->data.function.arg_count; + if (n == 0) return expr_new_integer(0); + + char* adj = calloc((size_t)n * (size_t)n, 1); + int has_edge = 0; + for (size_t k = 0; k < ne; k++) { + const Expr* ed = edges->data.function.args[k]; + int ia = graph_vertex_index(verts, ed->data.function.args[0]); + int ib = graph_vertex_index(verts, ed->data.function.args[1]); + if (ia < 0 || ib < 0 || ia == ib) continue; + adj[(size_t)ia * n + ib] = adj[(size_t)ib * n + ia] = 1; + has_edge = 1; + } + if (!has_edge) { free(adj); return expr_new_integer(1); } + + int* color = calloc((size_t)n, sizeof(int)); + ColorEnv e = { adj, n, 0, color }; + int chi = n; /* K_n upper bound; loop finds the least */ + for (int k = 2; k <= n; k++) { + e.k = k; + for (int i = 0; i < n; i++) color[i] = 0; + if (color_from(&e, 0, 0)) { chi = k; break; } + } + free(color); free(adj); + return expr_new_integer(chi); +} diff --git a/src/graph/chromaticpoly.c b/src/graph/chromaticpoly.c new file mode 100644 index 00000000..1a45c430 --- /dev/null +++ b/src/graph/chromaticpoly.c @@ -0,0 +1,87 @@ +/* chromaticpoly.c - ChromaticPolynomial[g, k]: the chromatic polynomial of g, + * the number of proper k-colourings as a polynomial in k. With a symbolic k it + * returns the polynomial (e.g. k(k-1)(k-2) for a triangle); with a numeric k it + * returns the colouring count. + * + * Computed from the subgraph-expansion (Whitney) form + * + * P(g, k) = sum_{S subset of E} (-1)^|S| * k^{c(S)} + * + * where c(S) is the number of connected components of the spanning subgraph + * (V, S). Collecting subsets by component count gives integer coefficients + * a_j = sum_{S : c(S)=j} (-1)^|S|, and the result is sum_j a_j k^j, assembled as + * an Expr and reduced by the evaluator -- so a symbolic k yields the polynomial + * and a numeric k yields the integer through one code path. Edge direction is + * ignored. + * + * The sum is over 2^|E| subsets, so it is exponential in the edge count; the + * call is left unevaluated when |E| exceeds a modest bound to avoid runaway + * work (small graphs -- the interactive/gallery case -- are unaffected). + * + * Memory (SPEC section 4): returns a fresh Expr; frees res. NULL on a non-graph + * or an oversized graph. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include "eval.h" +#include + +#define CHROM_MAX_EDGES 24 /* 2^24 subsets ~ 16.7M: the practical ceiling */ + +static int uf_find(int* p, int x) { while (p[x] != x) { p[x] = p[p[x]]; x = p[x]; } return x; } + +Expr* builtin_chromatic_polynomial(Expr* res) { + if (res->data.function.arg_count != 2) return NULL; + const Expr* g = res->data.function.args[0]; + const Expr* var = res->data.function.args[1]; + if (!graph_is_valid(g)) return NULL; + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + int n = (int)verts->data.function.arg_count; + int m = (int)edges->data.function.arg_count; + if (m > CHROM_MAX_EDGES) return NULL; /* too large; stay unevaluated */ + + /* Edge endpoints as vertex indices. */ + int* ea = (m > 0) ? malloc((size_t)m * sizeof(int)) : NULL; + int* eb = (m > 0) ? malloc((size_t)m * sizeof(int)) : NULL; + for (int k = 0; k < m; k++) { + const Expr* ed = edges->data.function.args[k]; + ea[k] = graph_vertex_index(verts, ed->data.function.args[0]); + eb[k] = graph_vertex_index(verts, ed->data.function.args[1]); + } + + /* coeff[j] = sum over subsets S with c(S)=j of (-1)^|S|, j = 0..n. */ + long* coeff = calloc((size_t)(n + 1), sizeof(long)); + int* parent = (n > 0) ? malloc((size_t)n * sizeof(int)) : NULL; + unsigned long subsets = 1UL << m; + for (unsigned long s = 0; s < subsets; s++) { + for (int i = 0; i < n; i++) parent[i] = i; + int comps = n, bits = 0; + for (int k = 0; k < m; k++) { + if (!(s & (1UL << k))) continue; + bits++; + int ra = uf_find(parent, ea[k]), rb = uf_find(parent, eb[k]); + if (ra != rb) { parent[ra] = rb; comps--; } + } + coeff[comps] += (bits & 1) ? -1 : 1; + } + free(ea); free(eb); free(parent); + + /* Build sum_j coeff[j] * var^j and let the evaluator reduce it. */ + Expr** terms = calloc((size_t)(n + 1), sizeof(Expr*)); + int nt = 0; + for (int j = 0; j <= n; j++) { + if (coeff[j] == 0) continue; + Expr* pw = expr_new_function(expr_new_symbol(SYM_Power), + (Expr*[]){ expr_copy((Expr*)var), expr_new_integer(j) }, 2); + terms[nt++] = expr_new_function(expr_new_symbol(SYM_Times), + (Expr*[]){ expr_new_integer(coeff[j]), pw }, 2); + } + free(coeff); + Expr* sum = expr_new_function(expr_new_symbol(SYM_Plus), terms, (size_t)nt); + free(terms); + return evaluate(sum); +} diff --git a/src/graph/circulantgraph.c b/src/graph/circulantgraph.c new file mode 100644 index 00000000..a5cb80a6 --- /dev/null +++ b/src/graph/circulantgraph.c @@ -0,0 +1,73 @@ +/* circulantgraph.c - CirculantGraph[n, {j1, j2, ...}] (or CirculantGraph[n, j]): + * the circulant graph on n vertices 1..n in which vertex i is joined to + * i +/- j (mod n) for each jump j in the list. + * + * Every vertex has the same neighbourhood pattern, so the graph is vertex- + * transitive and regular. Special cases: CirculantGraph[n, {1}] is the cycle + * C_n; CirculantGraph[n, {1,...,floor(n/2)}] is the complete graph K_n; a jump of + * exactly n/2 contributes a single (matching) edge per vertex rather than two. + * An n x n boolean adjacency dedups the +j / -j and cross-vertex coincidences. + * O(n * (#jumps) + n^2). + * + * Memory (SPEC section 4): returns a freshly-built Graph; frees res. NULL unless + * n >= 1 is an integer and the jumps are an integer (list). + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +Expr* builtin_circulant_graph(Expr* res) { + if (res->data.function.arg_count != 2) return NULL; + const Expr* ne = res->data.function.args[0]; + const Expr* js = res->data.function.args[1]; + if (ne->type != EXPR_INTEGER) return NULL; + long n = (long)ne->data.integer; + if (n < 1) return NULL; + + /* Collect jumps: a single integer or a List of integers. */ + const Expr* const* jump_src; + size_t njumps; + const Expr* single[1]; + if (js->type == EXPR_INTEGER) { + single[0] = js; jump_src = single; njumps = 1; + } else if (graph_is_list(js)) { + for (size_t i = 0; i < js->data.function.arg_count; i++) + if (js->data.function.args[i]->type != EXPR_INTEGER) return NULL; + jump_src = (const Expr* const*)js->data.function.args; + njumps = js->data.function.arg_count; + } else { + return NULL; + } + + char* adj = calloc((size_t)n * (size_t)n, 1); + for (long i = 0; i < n; i++) + for (size_t t = 0; t < njumps; t++) { + long j = (long)jump_src[t]->data.integer % n; + if (j < 0) j += n; + long a = (i + j) % n, b = (i - j % n + n) % n; + if (a != i) adj[(size_t)i * n + a] = adj[(size_t)a * n + i] = 1; + if (b != i) adj[(size_t)i * n + b] = adj[(size_t)b * n + i] = 1; + } + + Expr** vc = malloc((size_t)n * sizeof(Expr*)); + for (long i = 0; i < n; i++) vc[i] = expr_new_integer(i + 1); + + size_t cap = (size_t)n * (n > 0 ? (size_t)(n - 1) : 0) / 2; + Expr** ec = (cap > 0) ? malloc(cap * sizeof(Expr*)) : NULL; + size_t me = 0; + for (long i = 0; i < n; i++) + for (long k = i + 1; k < n; k++) + if (adj[(size_t)i * n + k]) { + Expr* args[2] = { expr_new_integer(i + 1), expr_new_integer(k + 1) }; + ec[me++] = expr_new_function(expr_new_symbol(SYM_UndirectedEdge), args, 2); + } + free(adj); + + Expr* vlist = expr_new_function(expr_new_symbol(SYM_List), vc, (size_t)n); + Expr* elist = expr_new_function(expr_new_symbol(SYM_List), ec, me); + free(vc); free(ec); + Expr* gargs[2] = { vlist, elist }; + return expr_new_function(expr_new_symbol(SYM_Graph), gargs, 2); +} diff --git a/src/graph/clustering.c b/src/graph/clustering.c new file mode 100644 index 00000000..0c2dd64c --- /dev/null +++ b/src/graph/clustering.c @@ -0,0 +1,65 @@ +/* clustering.c - LocalClusteringCoefficient[g]: for each vertex, the fraction of + * its neighbour pairs that are themselves adjacent -- the classic measure of how + * tightly a vertex's neighbourhood is knit. + * + * C_v = (edges among neighbours of v) / C(deg(v), 2) = 2 L_v / (d_v (d_v - 1)) + * + * and C_v = 0 when deg(v) < 2 (no pair to close). Edge direction is ignored (the + * coefficient is computed on the underlying undirected graph); results are exact + * rationals in vertex order. A symmetric boolean adjacency makes the + * neighbour-pair test O(1), so the whole vector costs O(V * d_max^2) <= O(V^3). + * + * Memory (SPEC section 4): returns a fresh List; frees res. NULL on a non-graph. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include "eval.h" +#include + +/* Exact num/den as a reduced value (integer or Rational). den > 0 required. */ +static Expr* ratio(long num, long den) { + Expr* pa[2] = { expr_new_integer(den), expr_new_integer(-1) }; + Expr* inv = expr_new_function(expr_new_symbol(SYM_Power), pa, 2); + Expr* ta[2] = { expr_new_integer(num), inv }; + return evaluate(expr_new_function(expr_new_symbol(SYM_Times), ta, 2)); +} + +Expr* builtin_local_clustering_coefficient(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + int n = (int)verts->data.function.arg_count; + size_t ne = edges->data.function.arg_count; + + char* adj = (n > 0) ? calloc((size_t)n * (size_t)n, 1) : NULL; + for (size_t e = 0; e < ne; e++) { + const Expr* ed = edges->data.function.args[e]; + int ia = graph_vertex_index(verts, ed->data.function.args[0]); + int ib = graph_vertex_index(verts, ed->data.function.args[1]); + if (ia < 0 || ib < 0 || ia == ib) continue; + adj[(size_t)ia * n + ib] = adj[(size_t)ib * n + ia] = 1; + } + + Expr** items = (n > 0) ? calloc((size_t)n, sizeof(Expr*)) : NULL; + int* nbr = (n > 0) ? malloc((size_t)n * sizeof(int)) : NULL; + for (int v = 0; v < n; v++) { + int d = 0; + for (int u = 0; u < n; u++) if (adj[(size_t)v * n + u]) nbr[d++] = u; + if (d < 2) { items[v] = expr_new_integer(0); continue; } + long links = 0; + for (int i = 0; i < d; i++) + for (int j = i + 1; j < d; j++) + if (adj[(size_t)nbr[i] * n + nbr[j]]) links++; + items[v] = ratio(2 * links, (long)d * (d - 1)); + } + free(nbr); free(adj); + + Expr* out = expr_new_function(expr_new_symbol(SYM_List), items, (size_t)n); + free(items); + return out; +} diff --git a/src/graph/cocktailparty.c b/src/graph/cocktailparty.c new file mode 100644 index 00000000..45d6100e --- /dev/null +++ b/src/graph/cocktailparty.c @@ -0,0 +1,46 @@ +/* cocktailparty.c - CocktailPartyGraph[n]: the cocktail-party graph K_{n x 2}, + * the complete n-partite graph with all n parts of size 2. Equivalently, 2n + * guests in n couples where everyone talks to everyone except their own partner + * -- the complement of a perfect matching on 2n vertices. + * + * Vertices are 1..2n; vertex i (0-based) is in couple i/2, and two vertices are + * joined iff they belong to different couples. It is (2n-2)-regular with + * 2n(n-1) edges. O(n^2). CocktailPartyGraph[2] = C4, CocktailPartyGraph[3] is the + * octahedron. + * + * Memory (SPEC section 4): returns a freshly-built Graph; frees res. NULL unless + * n >= 1 is an integer. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +Expr* builtin_cocktail_party_graph(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* ne = res->data.function.args[0]; + if (ne->type != EXPR_INTEGER) return NULL; + long n = (long)ne->data.integer; + if (n < 1) return NULL; + + long V = 2 * n; + Expr** vc = malloc((size_t)V * sizeof(Expr*)); + for (long i = 0; i < V; i++) vc[i] = expr_new_integer(i + 1); + + size_t cap = (size_t)(2 * n * (n - 1)); /* 2n(n-1) edges */ + Expr** ec = (cap > 0) ? malloc(cap * sizeof(Expr*)) : NULL; + size_t me = 0; + for (long i = 0; i < V; i++) + for (long j = i + 1; j < V; j++) + if (i / 2 != j / 2) { /* different couples */ + Expr* a[2] = { expr_new_integer(i + 1), expr_new_integer(j + 1) }; + ec[me++] = expr_new_function(expr_new_symbol(SYM_UndirectedEdge), a, 2); + } + + Expr* vlist = expr_new_function(expr_new_symbol(SYM_List), vc, (size_t)V); + Expr* elist = expr_new_function(expr_new_symbol(SYM_List), ec, me); + free(vc); free(ec); + Expr* gargs[2] = { vlist, elist }; + return expr_new_function(expr_new_symbol(SYM_Graph), gargs, 2); +} diff --git a/src/graph/complement.c b/src/graph/complement.c new file mode 100644 index 00000000..1edb728b --- /dev/null +++ b/src/graph/complement.c @@ -0,0 +1,73 @@ +/* complement.c - GraphComplement[g]: the graph on the same vertices whose edges + * are exactly the non-edges of g. + * + * undirected g -> undirected complement: every pair {i,j} (i complete K_n). + * directed g -> directed complement: every ordered pair (i,j), i != j, with + * no i->j edge becomes a DirectedEdge. + * No self-loops in either case. A graph with mixed edge kinds is complemented as + * undirected (its underlying undirected structure). + * + * Uses an n x n membership matrix, so it is O(V^2) — the natural cost, since the + * complement itself is dense. Returns the canonical Graph value directly. + * + * Memory (SPEC section 4): returns a freshly-built Graph tree; frees res. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +Expr* builtin_graph_complement(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + int n = (int)verts->data.function.arg_count; + size_t ne = edges->data.function.arg_count; + + /* Directed iff every edge is a DirectedEdge (edgeless is treated as + * undirected, so its complement is the complete graph). */ + int directed = (ne > 0); + for (size_t i = 0; i < ne; i++) + if (graph_edge_kind(edges->data.function.args[i]) != SYM_DirectedEdge) { directed = 0; break; } + + char* present = (n > 0) ? calloc((size_t)n * (size_t)n, 1) : NULL; + if (n > 0 && !present) return NULL; + for (size_t k = 0; k < ne; k++) { + const Expr* e = edges->data.function.args[k]; + int ia = graph_vertex_index(verts, e->data.function.args[0]); + int ib = graph_vertex_index(verts, e->data.function.args[1]); + if (ia < 0 || ib < 0) continue; + present[(size_t)ia * n + ib] = 1; + if (!directed) present[(size_t)ib * n + ia] = 1; + } + + /* Upper bound on complement edges: n(n-1) directed, n(n-1)/2 undirected. */ + size_t cap = directed ? (size_t)n * (n > 0 ? n - 1 : 0) + : (size_t)n * (n > 0 ? n - 1 : 0) / 2; + Expr** cedges = (cap > 0) ? calloc(cap, sizeof(Expr*)) : NULL; + size_t m = 0; + const char* ekind = directed ? SYM_DirectedEdge : SYM_UndirectedEdge; + for (int i = 0; i < n; i++) { + for (int j = directed ? 0 : i + 1; j < n; j++) { + if (i == j) continue; + if (present[(size_t)i * n + j]) continue; + Expr* args[2] = { expr_copy(verts->data.function.args[i]), + expr_copy(verts->data.function.args[j]) }; + cedges[m++] = expr_new_function(expr_new_symbol(ekind), args, 2); + } + } + free(present); + + Expr** vcopy = (n > 0) ? calloc((size_t)n, sizeof(Expr*)) : NULL; + for (int i = 0; i < n; i++) vcopy[i] = expr_copy(verts->data.function.args[i]); + Expr* vlist = expr_new_function(expr_new_symbol(SYM_List), vcopy, (size_t)n); + Expr* elist = expr_new_function(expr_new_symbol(SYM_List), cedges, m); + free(vcopy); free(cedges); + Expr* gargs[2] = { vlist, elist }; + return expr_new_function(expr_new_symbol(SYM_Graph), gargs, 2); +} diff --git a/src/graph/completegraphq.c b/src/graph/completegraphq.c new file mode 100644 index 00000000..bb26237f --- /dev/null +++ b/src/graph/completegraphq.c @@ -0,0 +1,42 @@ +/* completegraphq.c - CompleteGraphQ[g]: True iff g is complete -- every pair of + * distinct vertices is adjacent (the underlying undirected graph is K_n). + * + * Build a boolean adjacency (direction ignored, a->b or b->a both count) and + * check that all n(n-1)/2 unordered pairs are present. Equivalently, the number + * of distinct undirected edges equals n(n-1)/2. O(V^2). A graph with <= 1 vertex + * is vacuously complete (K_0, K_1). + * + * Memory (SPEC section 4): returns True/False; frees res. NULL on a non-graph. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +Expr* builtin_complete_graph_q(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + int n = (int)verts->data.function.arg_count; + size_t ne = edges->data.function.arg_count; + + char* adj = (n > 0) ? calloc((size_t)n * (size_t)n, 1) : NULL; + for (size_t k = 0; k < ne; k++) { + const Expr* e = edges->data.function.args[k]; + int ia = graph_vertex_index(verts, e->data.function.args[0]); + int ib = graph_vertex_index(verts, e->data.function.args[1]); + if (ia < 0 || ib < 0 || ia == ib) continue; + adj[(size_t)ia * n + ib] = adj[(size_t)ib * n + ia] = 1; + } + + int complete = 1; + for (int i = 0; i < n && complete; i++) + for (int j = i + 1; j < n; j++) + if (!adj[(size_t)i * n + j]) { complete = 0; break; } + free(adj); + return expr_new_symbol(complete ? SYM_True : SYM_False); +} diff --git a/src/graph/completekarytree.c b/src/graph/completekarytree.c new file mode 100644 index 00000000..a878c21f --- /dev/null +++ b/src/graph/completekarytree.c @@ -0,0 +1,68 @@ +/* completekarytree.c - CompleteKaryTree[L] / CompleteKaryTree[L, k]: the complete + * k-ary tree with L levels (k = 2 by default) -- a root whose every internal node + * has exactly k children, filled to depth L. + * + * Vertices are 1..V in breadth-first (heap) order, so vertex i >= 2 has parent + * floor((i-2)/k) + 1; the V-1 tree edges are (parent(i), i), undirected. The + * vertex count is V = (k^L - 1)/(k - 1) for k >= 2 and V = L for k = 1 (a path). + * O(V). The result is always a tree (TreeGraphQ is True). + * + * L >= 1 and k >= 1 must be integers; the call is left unevaluated if the tree + * would be larger than a safety cap. + * + * Memory (SPEC section 4): returns a freshly-built Graph; frees res. NULL on bad + * arguments. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +#define KARY_MAX_VERTICES 1000000L + +Expr* builtin_complete_kary_tree(Expr* res) { + size_t argc = res->data.function.arg_count; + if (argc != 1 && argc != 2) return NULL; + const Expr* Le = res->data.function.args[0]; + if (Le->type != EXPR_INTEGER) return NULL; + long L = (long)Le->data.integer; + long k = 2; + if (argc == 2) { + const Expr* ke = res->data.function.args[1]; + if (ke->type != EXPR_INTEGER) return NULL; + k = (long)ke->data.integer; + } + if (L < 1 || k < 1) return NULL; + + /* Vertex count V, with overflow/size guard. */ + long V; + if (k == 1) { + V = L; + } else { + V = 1; long level = 1; + for (long d = 1; d < L; d++) { + level *= k; /* nodes at depth d */ + if (level > KARY_MAX_VERTICES || V > KARY_MAX_VERTICES - level) return NULL; + V += level; + } + } + if (V > KARY_MAX_VERTICES) return NULL; + + Expr** vc = malloc((size_t)V * sizeof(Expr*)); + for (long i = 0; i < V; i++) vc[i] = expr_new_integer(i + 1); + + Expr** ec = (V > 1) ? malloc((size_t)(V - 1) * sizeof(Expr*)) : NULL; + size_t me = 0; + for (long i = 2; i <= V; i++) { + long parent = (i - 2) / k + 1; + Expr* args[2] = { expr_new_integer(parent), expr_new_integer(i) }; + ec[me++] = expr_new_function(expr_new_symbol(SYM_UndirectedEdge), args, 2); + } + + Expr* vlist = expr_new_function(expr_new_symbol(SYM_List), vc, (size_t)V); + Expr* elist = expr_new_function(expr_new_symbol(SYM_List), ec, me); + free(vc); free(ec); + Expr* gargs[2] = { vlist, elist }; + return expr_new_function(expr_new_symbol(SYM_Graph), gargs, 2); +} diff --git a/src/graph/components.c b/src/graph/components.c new file mode 100644 index 00000000..675e6e2a --- /dev/null +++ b/src/graph/components.c @@ -0,0 +1,140 @@ +/* components.c - connected-component builtins. + * + * ConnectedComponents[g] weak components (underlying undirected) + * WeaklyConnectedComponents[g] same as ConnectedComponents + * StronglyConnectedComponents[g] strong components (Tarjan) over directed + * adjacency; for undirected graphs this + * coincides with the weak components. + * + * Each returns a List of Lists of vertices, components in first-appearance + * order, vertices within a component in canonical index order. + * + * Memory (SPEC section 4): returns freshly-allocated lists; frees res. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +/* Build the result List from a component-id labeling comp[0..n-1] with `k` + * components. Components appear in order of their first vertex; vertices within + * each keep index order. */ +static Expr* components_to_list(const GraphAdj* a, const int* comp, int k) { + int n = a->n; + /* First-appearance order of component ids. */ + int* order = calloc((size_t)(k > 0 ? k : 1), sizeof(int)); + int* pos = malloc((size_t)(k > 0 ? k : 1) * sizeof(int)); + for (int i = 0; i < k; i++) pos[i] = -1; + int seen = 0; + for (int i = 0; i < n; i++) + if (pos[comp[i]] < 0) { pos[comp[i]] = seen; order[seen++] = comp[i]; } + + Expr** groups = calloc((size_t)(k > 0 ? k : 1), sizeof(Expr*)); + for (int gi = 0; gi < k; gi++) { + int cid = order[gi]; + int cnt = 0; + for (int i = 0; i < n; i++) if (comp[i] == cid) cnt++; + Expr** members = (cnt > 0) ? calloc((size_t)cnt, sizeof(Expr*)) : NULL; + int m = 0; + for (int i = 0; i < n; i++) + if (comp[i] == cid) members[m++] = expr_copy(a->verts->data.function.args[i]); + groups[gi] = expr_new_function(expr_new_symbol(SYM_List), members, (size_t)cnt); + free(members); + } + Expr* out = expr_new_function(expr_new_symbol(SYM_List), groups, (size_t)k); + free(groups); free(order); free(pos); + return out; +} + +/* Weak/underlying-undirected labeling via DFS over out+in. */ +static Expr* weak_components(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + GraphAdj* a = graph_build_adj(res->data.function.args[0]); + if (!a) return NULL; + int n = a->n; + int* comp = malloc((size_t)(n > 0 ? n : 1) * sizeof(int)); + for (int i = 0; i < n; i++) comp[i] = -1; + int* stack = calloc((size_t)(n > 0 ? n : 1), sizeof(int)); + int k = 0; + for (int s = 0; s < n; s++) { + if (comp[s] >= 0) continue; + int top = 0; stack[top++] = s; comp[s] = k; + while (top > 0) { + int u = stack[--top]; + for (int j = 0; j < a->outdeg[u]; j++) { int w = a->out[u][j]; if (comp[w] < 0) { comp[w] = k; stack[top++] = w; } } + for (int j = 0; j < a->indeg[u]; j++) { int w = a->in[u][j]; if (comp[w] < 0) { comp[w] = k; stack[top++] = w; } } + } + k++; + } + Expr* out = components_to_list(a, comp, k); + free(comp); free(stack); graph_adj_free(a); + return out; +} + +Expr* builtin_connected_components(Expr* res) { return weak_components(res); } +Expr* builtin_weakly_connected_components(Expr* res) { return weak_components(res); } + +/* ---- Tarjan strongly-connected components --------------------------------- */ +typedef struct { + const GraphAdj* a; + int* index; int* low; char* onstack; int* stack; int sp; + int* comp; int counter; int k; +} Tarjan; + +/* Iterative Tarjan to avoid deep recursion on large graphs. */ +static void tarjan_run(Tarjan* t) { + int n = t->a->n; + int* it = calloc((size_t)(n > 0 ? n : 1), sizeof(int)); /* per-node child cursor */ + int* callstk = calloc((size_t)(n > 0 ? n : 1), sizeof(int)); + for (int s = 0; s < n; s++) { + if (t->index[s] >= 0) continue; + int csp = 0; callstk[csp++] = s; + t->index[s] = t->low[s] = t->counter++; + t->stack[t->sp++] = s; t->onstack[s] = 1; + while (csp > 0) { + int u = callstk[csp - 1]; + if (it[u] < t->a->outdeg[u]) { + int w = t->a->out[u][it[u]++]; + if (t->index[w] < 0) { + t->index[w] = t->low[w] = t->counter++; + t->stack[t->sp++] = w; t->onstack[w] = 1; + callstk[csp++] = w; + } else if (t->onstack[w]) { + if (t->index[w] < t->low[u]) t->low[u] = t->index[w]; + } + } else { + /* Done with u: if root of an SCC, pop it. */ + if (t->low[u] == t->index[u]) { + int w; + do { w = t->stack[--t->sp]; t->onstack[w] = 0; t->comp[w] = t->k; } while (w != u); + t->k++; + } + csp--; + if (csp > 0) { int p = callstk[csp - 1]; if (t->low[u] < t->low[p]) t->low[p] = t->low[u]; } + } + } + } + free(it); free(callstk); +} + +Expr* builtin_strongly_connected_components(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + GraphAdj* a = graph_build_adj(res->data.function.args[0]); + if (!a) return NULL; + int n = a->n; + Tarjan t; + t.a = a; + t.index = malloc((size_t)(n > 0 ? n : 1) * sizeof(int)); + t.low = malloc((size_t)(n > 0 ? n : 1) * sizeof(int)); + t.onstack = calloc((size_t)(n > 0 ? n : 1), sizeof(char)); + t.stack = calloc((size_t)(n > 0 ? n : 1), sizeof(int)); + t.comp = malloc((size_t)(n > 0 ? n : 1) * sizeof(int)); + t.sp = 0; t.counter = 0; t.k = 0; + for (int i = 0; i < n; i++) { t.index[i] = -1; t.low[i] = -1; t.comp[i] = -1; } + tarjan_run(&t); + Expr* out = components_to_list(a, t.comp, t.k); + free(t.index); free(t.low); free(t.onstack); free(t.stack); free(t.comp); + graph_adj_free(a); + return out; +} diff --git a/src/graph/connectivity.c b/src/graph/connectivity.c new file mode 100644 index 00000000..eaa3ac8e --- /dev/null +++ b/src/graph/connectivity.c @@ -0,0 +1,75 @@ +/* connectivity.c - ConnectedGraphQ[g] and VertexConnectivity[g]. + * + * Both operate on the underlying undirected graph. + * ConnectedGraphQ[g] True iff g has >= 1 vertex and forms a single + * connected component. + * VertexConnectivity[g] the least number of vertices whose removal + * disconnects g (n-1 for a complete graph, 0 if already + * disconnected or trivial). Computed by brute-force + * search over vertex subsets -- exact, intended for the + * small graphs of a pico-CAS. + * + * Memory (SPEC section 4): returns freshly-allocated results; frees res. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +Expr* builtin_connected_graph_q(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + GraphAdj* a = graph_build_adj(res->data.function.args[0]); + if (!a) return NULL; + int comps = graph_count_components(a, NULL, NULL); + int connected = (a->n >= 1 && comps == 1); + graph_adj_free(a); + return expr_new_symbol(connected ? SYM_True : SYM_False); +} + +/* Does some size-k subset of vertices disconnect the graph? Enumerates all + * C(n,k) subsets via an index array; sets `removed` for each. */ +static int some_cut_of_size(const GraphAdj* a, int k, char* removed) { + int n = a->n; + int* idx = calloc((size_t)(k > 0 ? k : 1), sizeof(int)); + for (int i = 0; i < k; i++) idx[i] = i; + int found = 0; + while (!found) { + for (int i = 0; i < n; i++) removed[i] = 0; + for (int i = 0; i < k; i++) removed[idx[i]] = 1; + int active = 0; + int comps = graph_count_components(a, removed, &active); + if (active >= 2 && comps >= 2) { found = 1; break; } + /* Advance the combination (rightmost index that can move). */ + int p = k - 1; + while (p >= 0 && idx[p] == n - k + p) p--; + if (p < 0) break; + idx[p]++; + for (int i = p + 1; i < k; i++) idx[i] = idx[i - 1] + 1; + } + free(idx); + return found; +} + +Expr* builtin_vertex_connectivity(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + GraphAdj* a = graph_build_adj(res->data.function.args[0]); + if (!a) return NULL; + int n = a->n; + + int kappa; + if (n <= 1) { + kappa = 0; + } else if (graph_count_components(a, NULL, NULL) > 1) { + kappa = 0; /* already disconnected */ + } else { + char* removed = calloc((size_t)n, sizeof(char)); + kappa = n - 1; /* complete-graph fallback */ + for (int k = 1; k <= n - 2; k++) { + if (some_cut_of_size(a, k, removed)) { kappa = k; break; } + } + free(removed); + } + graph_adj_free(a); + return expr_new_integer(kappa); +} diff --git a/src/graph/construct.c b/src/graph/construct.c new file mode 100644 index 00000000..a11b370e --- /dev/null +++ b/src/graph/construct.c @@ -0,0 +1,158 @@ +/* construct.c - builtin_graph: normalize, derive, validate, canonicalize. + * + * Accepts: + * Graph[edges] -- vertices derived from the edges (directed default) + * Graph[verts, edges] -- explicit vertex list + * + * Edge sugar is normalized on construction: + * Rule[u,v] / u -> v -> DirectedEdge[u, v] + * TwoWayRule[u,v] / u <-> v -> UndirectedEdge[u, v] + * DirectedEdge[u,v] / UndirectedEdge[u,v] pass through unchanged + * + * The result is the canonical Graph[List[verts], List[edges]] with vertices in + * first-appearance order (when derived). Malformed input -- 3-arg edges, + * self-loops, parallel edges, or an edge endpoint absent from an explicit + * vertex list -- leaves Graph[...] unevaluated (returns NULL). + * + * Memory (SPEC section 4): the canonical tree is built entirely from expr_copy + * of the argument's parts, so `res` is never cannibalized. On success the + * evaluator frees `res`; on NULL it retains it. The "already canonical" case + * returns NULL so evaluation reaches a fixed point. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +/* head symbol of a function node, or NULL. */ +static const char* fn_head(const Expr* e) { + if (!e || e->type != EXPR_FUNCTION || !e->data.function.head + || e->data.function.head->type != EXPR_SYMBOL) + return NULL; + return e->data.function.head->data.symbol; +} + +/* Build a fresh normalized edge (with copied endpoints) from any accepted edge + * form, or NULL if `e` is not a recognizable 2-argument edge. Does not reject + * self-loops -- the caller does, via graph_is_valid. */ +static Expr* normalize_edge(const Expr* e) { + const char* h = fn_head(e); + if (!h || e->data.function.arg_count != 2) return NULL; + + const char* out_head; + if (h == SYM_Rule || h == SYM_DirectedEdge) out_head = SYM_DirectedEdge; + else if (h == SYM_TwoWayRule || h == SYM_UndirectedEdge) out_head = SYM_UndirectedEdge; + else return NULL; + + Expr* args[2]; + args[0] = expr_copy(e->data.function.args[0]); + args[1] = expr_copy(e->data.function.args[1]); + return expr_new_function(expr_new_symbol(out_head), args, 2); +} + +/* Assemble the canonical Graph from `res`, or NULL if the shape is wrong or the + * result would be invalid. */ +static Expr* try_build_canonical_head(Expr* res, const char* head_sym) { + size_t argc = res->data.function.arg_count; + const Expr* verts_in = NULL; + const Expr* edges_in = NULL; + + if (argc == 1) { + const Expr* arg = res->data.function.args[0]; + const char* ah = fn_head(arg); + /* Graph3D[existing graph] / Graph[graph]: reuse its vertex & edge + * lists (converts a 2D graph to 3D and vice versa). */ + if ((ah == SYM_Graph || ah == SYM_Graph3D) + && arg->data.function.arg_count == 2 + && graph_is_list(arg->data.function.args[0]) + && graph_is_list(arg->data.function.args[1])) { + verts_in = arg->data.function.args[0]; + edges_in = arg->data.function.args[1]; + } else { + edges_in = arg; + } + } else if (argc == 2) { + verts_in = res->data.function.args[0]; + edges_in = res->data.function.args[1]; + } else { + return NULL; + } + if (!graph_is_list(edges_in)) return NULL; + if (verts_in && !graph_is_list(verts_in)) return NULL; + + size_t ne = edges_in->data.function.arg_count; + + /* 1. Normalize every edge. */ + Expr** edges = (ne > 0) ? calloc(ne, sizeof(Expr*)) : NULL; + if (ne > 0 && !edges) return NULL; + for (size_t i = 0; i < ne; i++) { + edges[i] = normalize_edge(edges_in->data.function.args[i]); + if (!edges[i]) { + for (size_t j = 0; j < i; j++) expr_free(edges[j]); + free(edges); + return NULL; + } + } + + /* 2. Build the vertex list: copy the explicit one, or derive it from the + * normalized edges in first-appearance order. */ + Expr** verts = NULL; + size_t nv = 0; + if (verts_in) { + nv = verts_in->data.function.arg_count; + verts = (nv > 0) ? calloc(nv, sizeof(Expr*)) : NULL; + if (nv > 0 && !verts) goto fail_edges; + for (size_t i = 0; i < nv; i++) + verts[i] = expr_copy(verts_in->data.function.args[i]); + } else { + /* At most 2 distinct new vertices per edge. */ + verts = (ne > 0) ? calloc(ne * 2, sizeof(Expr*)) : NULL; + if (ne > 0 && !verts) goto fail_edges; + for (size_t i = 0; i < ne; i++) { + for (int k = 0; k < 2; k++) { + Expr* ep = edges[i]->data.function.args[k]; + int seen = 0; + for (size_t j = 0; j < nv; j++) + if (expr_eq(verts[j], ep)) { seen = 1; break; } + if (!seen) verts[nv++] = expr_copy(ep); + } + } + } + + /* 3. Assemble candidate Graph[List verts, List edges] (moves ownership). */ + Expr* vlist = expr_new_function(expr_new_symbol(SYM_List), verts, nv); + Expr* elist = expr_new_function(expr_new_symbol(SYM_List), edges, ne); + free(verts); + free(edges); + Expr* gargs[2] = { vlist, elist }; + Expr* g = expr_new_function(expr_new_symbol(head_sym), gargs, 2); + + /* 4. Validate (self-loops, parallel edges, endpoint membership). */ + if (!graph_is_valid_head(g, head_sym)) { expr_free(g); return NULL; } + return g; + +fail_edges: + for (size_t i = 0; i < ne; i++) expr_free(edges[i]); + free(edges); + return NULL; +} + +Expr* builtin_graph(Expr* res) { + Expr* canonical = try_build_canonical_head(res, SYM_Graph); + if (!canonical) return NULL; /* malformed: leave unevaluated */ + if (expr_eq(canonical, res)) { /* already canonical: fixed point */ + expr_free(canonical); + return NULL; + } + return canonical; +} + +/* Graph3D[...] normalizes/validates exactly like Graph but produces a value + * with head Graph3D, which the REPL auto-displays as a 3D node-link diagram. */ +Expr* builtin_graph3d(Expr* res) { + Expr* canonical = try_build_canonical_head(res, SYM_Graph3D); + if (!canonical) return NULL; + if (expr_eq(canonical, res)) { expr_free(canonical); return NULL; } + return canonical; +} diff --git a/src/graph/counts.c b/src/graph/counts.c new file mode 100644 index 00000000..8263e2a2 --- /dev/null +++ b/src/graph/counts.c @@ -0,0 +1,20 @@ +/* counts.c - VertexCount[g] and EdgeCount[g]: cardinalities as integers. + * Thin readers over the canonical form; NULL (unevaluated) if g is not a graph. + * Memory (SPEC section 4): returns a fresh integer; the evaluator frees res. */ + +#include "graph.h" +#include "expr.h" + +Expr* builtin_vertex_count(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + return expr_new_integer((int64_t)g->data.function.args[0]->data.function.arg_count); +} + +Expr* builtin_edge_count(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + return expr_new_integer((int64_t)g->data.function.args[1]->data.function.arg_count); +} diff --git a/src/graph/degree.c b/src/graph/degree.c new file mode 100644 index 00000000..448a9ef3 --- /dev/null +++ b/src/graph/degree.c @@ -0,0 +1,74 @@ +/* degree.c - VertexDegree, VertexInDegree, VertexOutDegree. + * + * Each accepts VertexDegree[g] (a list of degrees, one per vertex in canonical + * order) or VertexDegree[g, v] (the degree of a single vertex). + * + * Conventions (documented in docs/spec/builtins/graphs.md): + * - A DirectedEdge[a,b] adds 1 to out(a) and 1 to in(b); total degree of a + * vertex is in + out, so for a purely directed graph total = in + out. + * - An UndirectedEdge[a,b] is incident to both a and b, adding 1 to each of + * their in-, out-, and total degrees (so in = out = total for a purely + * undirected graph). + * There are no self-loops, so no endpoint is double-counted within one edge. + * + * Memory (SPEC section 4): returns freshly-allocated integers/lists; the + * evaluator frees res. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +enum { DEG_TOTAL, DEG_IN, DEG_OUT }; + +/* Degree of vertex `v` in graph `g` under the given mode. */ +static int64_t degree_of(const Expr* g, const Expr* v, int mode) { + const Expr* edges = g->data.function.args[1]; + int64_t in = 0, out = 0, tot = 0; + for (size_t i = 0; i < edges->data.function.arg_count; i++) { + const Expr* e = edges->data.function.args[i]; + const char* kind = graph_edge_kind(e); + const Expr* a = e->data.function.args[0]; + const Expr* b = e->data.function.args[1]; + int va = expr_eq(a, v); + int vb = expr_eq(b, v); + if (kind == SYM_UndirectedEdge) { + /* Incident once: contributes to in, out, and total alike. */ + if (va || vb) { in++; out++; tot++; } + } else { /* directed: total counts each incident edge once */ + if (va) { out++; tot++; } + if (vb) { in++; tot++; } + } + } + if (mode == DEG_IN) return in; + if (mode == DEG_OUT) return out; + return tot; +} + +static Expr* degree_dispatch(Expr* res, int mode) { + size_t argc = res->data.function.arg_count; + if (argc < 1 || argc > 2) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + const Expr* verts = g->data.function.args[0]; + + if (argc == 2) { + const Expr* v = res->data.function.args[1]; + if (graph_vertex_index(verts, v) < 0) return NULL; /* v not a vertex */ + return expr_new_integer(degree_of(g, v, mode)); + } + + size_t nv = verts->data.function.arg_count; + Expr** out = (nv > 0) ? calloc(nv, sizeof(Expr*)) : NULL; + if (nv > 0 && !out) return NULL; + for (size_t i = 0; i < nv; i++) + out[i] = expr_new_integer(degree_of(g, verts->data.function.args[i], mode)); + Expr* list = expr_new_function(expr_new_symbol(SYM_List), out, nv); + free(out); + return list; +} + +Expr* builtin_vertex_degree(Expr* res) { return degree_dispatch(res, DEG_TOTAL); } +Expr* builtin_vertex_in_degree(Expr* res) { return degree_dispatch(res, DEG_IN); } +Expr* builtin_vertex_out_degree(Expr* res) { return degree_dispatch(res, DEG_OUT); } diff --git a/src/graph/degreecentrality.c b/src/graph/degreecentrality.c new file mode 100644 index 00000000..b700ed84 --- /dev/null +++ b/src/graph/degreecentrality.c @@ -0,0 +1,46 @@ +/* degreecentrality.c - DegreeCentrality[g]: for each vertex, the number of + * edges incident to it -- the simplest centrality measure, completing the trio + * with ClosenessCentrality and BetweennessCentrality. + * + * For an undirected graph this is the ordinary degree; for a directed graph it + * is in-degree + out-degree. Both fall out of one rule -- every edge increments + * a counter at each of its two endpoints -- since an undirected edge touches + * each endpoint once (degree) and a directed edge a->b contributes one to a's + * out-count and one to b's in-count. So no direction test is needed. O(V+E). + * + * Results are exact integers in vertex (canonical) order. + * + * Memory (SPEC section 4): returns a fresh List; frees res. NULL on a non-graph. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +Expr* builtin_degree_centrality(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + int n = (int)verts->data.function.arg_count; + size_t ne = edges->data.function.arg_count; + + long* deg = (n > 0) ? calloc((size_t)n, sizeof(long)) : NULL; + for (size_t k = 0; k < ne; k++) { + const Expr* e = edges->data.function.args[k]; + int ia = graph_vertex_index(verts, e->data.function.args[0]); + int ib = graph_vertex_index(verts, e->data.function.args[1]); + if (ia >= 0) deg[ia]++; + if (ib >= 0) deg[ib]++; + } + + Expr** items = (n > 0) ? calloc((size_t)n, sizeof(Expr*)) : NULL; + for (int i = 0; i < n; i++) items[i] = expr_new_integer(deg[i]); + free(deg); + Expr* out = expr_new_function(expr_new_symbol(SYM_List), items, (size_t)n); + free(items); + return out; +} diff --git a/src/graph/degreesequence.c b/src/graph/degreesequence.c new file mode 100644 index 00000000..b28077dc --- /dev/null +++ b/src/graph/degreesequence.c @@ -0,0 +1,50 @@ +/* degreesequence.c - DegreeSequence[g]: the vertex degrees sorted in + * non-increasing order. For an undirected graph this is the ordinary degree + * sequence; for a directed graph it is the total degree (in + out) per vertex, + * sorted descending. + * + * Every edge increments a counter at each of its two endpoints (the same rule + * as DegreeCentrality: an undirected edge touches each endpoint once = degree, + * a directed edge a->b adds to a's out-count and b's in-count = in+out), then + * the counts are sorted high-to-low. O(V + E + V log V). Exact integers. + * + * Memory (SPEC section 4): returns a fresh List; frees res. NULL on a non-graph. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +static int cmp_desc(const void* a, const void* b) { + long x = *(const long*)a, y = *(const long*)b; + return (x < y) - (x > y); /* descending */ +} + +Expr* builtin_degree_sequence(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + int n = (int)verts->data.function.arg_count; + size_t ne = edges->data.function.arg_count; + + long* deg = (n > 0) ? calloc((size_t)n, sizeof(long)) : NULL; + for (size_t k = 0; k < ne; k++) { + const Expr* e = edges->data.function.args[k]; + int ia = graph_vertex_index(verts, e->data.function.args[0]); + int ib = graph_vertex_index(verts, e->data.function.args[1]); + if (ia >= 0) deg[ia]++; + if (ib >= 0) deg[ib]++; + } + if (n > 0) qsort(deg, (size_t)n, sizeof(long), cmp_desc); + + Expr** items = (n > 0) ? calloc((size_t)n, sizeof(Expr*)) : NULL; + for (int i = 0; i < n; i++) items[i] = expr_new_integer(deg[i]); + free(deg); + Expr* out = expr_new_function(expr_new_symbol(SYM_List), items, (size_t)n); + free(items); + return out; +} diff --git a/src/graph/density.c b/src/graph/density.c new file mode 100644 index 00000000..a4dc9390 --- /dev/null +++ b/src/graph/density.c @@ -0,0 +1,47 @@ +/* density.c - GraphDensity[g]: the fraction of possible edges that are present, + * an exact rational in [0, 1]. + * + * undirected g -> m / C(n,2) = 2m / (n(n-1)) (n possible unordered pairs) + * directed g -> m / (n(n-1)) (ordered pairs, no self-loops) + * + * A graph with fewer than two vertices has no possible edges; its density is 0 + * by convention. The value is built as num/den and reduced by the evaluator, so + * it prints as a clean integer or Rational (K_n -> 1, an empty graph -> 0). + * + * Memory (SPEC section 4): returns a fresh number; frees res. NULL on a + * non-graph argument. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include "eval.h" +#include + +/* Exact num/den as a reduced value (integer or Rational). den > 0 required. */ +static Expr* ratio(long num, long den) { + Expr* pa[2] = { expr_new_integer(den), expr_new_integer(-1) }; + Expr* inv = expr_new_function(expr_new_symbol(SYM_Power), pa, 2); + Expr* ta[2] = { expr_new_integer(num), inv }; + return evaluate(expr_new_function(expr_new_symbol(SYM_Times), ta, 2)); +} + +Expr* builtin_graph_density(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + long n = (long)verts->data.function.arg_count; + long ne = (long)edges->data.function.arg_count; + if (n < 2) return expr_new_integer(0); + + int directed = (ne > 0); + for (long i = 0; i < ne; i++) + if (graph_edge_kind(edges->data.function.args[i]) != SYM_DirectedEdge) { directed = 0; break; } + + /* directed: n(n-1) ordered pairs; undirected: n(n-1)/2 unordered pairs. */ + if (directed) return ratio(ne, n * (n - 1)); + return ratio(2 * ne, n * (n - 1)); +} diff --git a/src/graph/directedq.c b/src/graph/directedq.c new file mode 100644 index 00000000..4ec5f52c --- /dev/null +++ b/src/graph/directedq.c @@ -0,0 +1,20 @@ +/* directedq.c - DirectedGraphQ[g]: True iff g is a valid graph whose edges are + * all DirectedEdge (vacuously True for an edgeless graph). False otherwise. + * Memory (SPEC section 4): returns a fresh symbol; the evaluator frees res. */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" + +Expr* builtin_directed_graph_q(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return expr_new_symbol(SYM_False); + + const Expr* edges = g->data.function.args[1]; + for (size_t i = 0; i < edges->data.function.arg_count; i++) { + if (graph_edge_kind(edges->data.function.args[i]) != SYM_DirectedEdge) + return expr_new_symbol(SYM_False); + } + return expr_new_symbol(SYM_True); +} diff --git a/src/graph/distmatrix.c b/src/graph/distmatrix.c new file mode 100644 index 00000000..4bfec0b9 --- /dev/null +++ b/src/graph/distmatrix.c @@ -0,0 +1,57 @@ +/* distmatrix.c - GraphDistanceMatrix[g]: the n x n matrix whose (i,j) entry is + * the shortest-path distance from vertex i to vertex j -- 0 on the diagonal and + * Infinity when j is unreachable from i. The all-pairs companion to + * GraphDistance[g, s, t] and the distance metrics in metrics.c. + * + * One breadth-first search per source over the successor adjacency (out[]): for + * a directed graph this follows edge direction (so the matrix is generally + * asymmetric), for an undirected graph out[] is symmetric so it is ordinary + * shortest-path distance. Complexity O(V*(V+E)). + * + * Memory (SPEC section 4): returns a freshly-built List of Lists; frees res. + * Returns NULL (unevaluated) on a non-graph argument. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +Expr* builtin_graph_distance_matrix(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + + GraphAdj* a = graph_build_adj(g); + if (!a) return NULL; + int n = a->n; + + int* dist = (n > 0) ? malloc((size_t)n * sizeof(int)) : NULL; + int* q = (n > 0) ? malloc((size_t)n * sizeof(int)) : NULL; + + Expr** rows = (n > 0) ? calloc((size_t)n, sizeof(Expr*)) : NULL; + for (int s = 0; s < n; s++) { + for (int i = 0; i < n; i++) dist[i] = -1; + int head = 0, tail = 0; + dist[s] = 0; q[tail++] = s; + while (head < tail) { + int u = q[head++]; + for (int j = 0; j < a->outdeg[u]; j++) { + int w = a->out[u][j]; + if (dist[w] < 0) { dist[w] = dist[u] + 1; q[tail++] = w; } + } + } + Expr** entries = (n > 0) ? calloc((size_t)n, sizeof(Expr*)) : NULL; + for (int t = 0; t < n; t++) + entries[t] = (dist[t] < 0) ? expr_new_symbol(SYM_Infinity) + : expr_new_integer(dist[t]); + rows[s] = expr_new_function(expr_new_symbol(SYM_List), entries, (size_t)n); + free(entries); + } + free(dist); free(q); + graph_adj_free(a); + + Expr* out = expr_new_function(expr_new_symbol(SYM_List), rows, (size_t)n); + free(rows); + return out; +} diff --git a/src/graph/dodecahedralgraph.c b/src/graph/dodecahedralgraph.c new file mode 100644 index 00000000..be2a8ab1 --- /dev/null +++ b/src/graph/dodecahedralgraph.c @@ -0,0 +1,41 @@ +/* dodecahedralgraph.c - DodecahedralGraph[]: the graph of the regular + * dodecahedron (one of the five Platonic solids), which is exactly the + * generalized Petersen graph GP(10, 2). + * + * 20 vertices (outer 10-cycle 1..10, inner set 11..20) and 30 edges: the outer + * cycle o_j ~ o_{j+1}, the spokes o_j ~ i_j, and the inner star polygon + * i_j ~ i_{j+2} (indices mod 10). 3-regular, girth 5, non-bipartite, connected. + * Takes no arguments. O(1). + * + * Memory (SPEC section 4): returns a freshly-built Graph; frees res. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +Expr* builtin_dodecahedral_graph(Expr* res) { + if (res->data.function.arg_count != 0) return NULL; + + enum { N = 10, V = 20 }; + Expr** vc = malloc(V * sizeof(Expr*)); + for (int i = 0; i < V; i++) vc[i] = expr_new_integer(i + 1); + + Expr** ec = malloc(30 * sizeof(Expr*)); + size_t me = 0; + #define ADD(a,b) do { Expr* _e[2] = { expr_new_integer(a), expr_new_integer(b) }; \ + ec[me++] = expr_new_function(expr_new_symbol(SYM_UndirectedEdge), _e, 2); } while (0) + for (int j = 0; j < N; j++) { + ADD(j + 1, (j + 1) % N + 1); /* outer cycle */ + ADD(j + 1, N + 1 + j); /* spoke */ + ADD(N + 1 + j, N + 1 + (j + 2) % N); /* inner star polygon {10/2} */ + } + #undef ADD + + Expr* vlist = expr_new_function(expr_new_symbol(SYM_List), vc, (size_t)V); + Expr* elist = expr_new_function(expr_new_symbol(SYM_List), ec, me); + free(vc); free(ec); + Expr* gargs[2] = { vlist, elist }; + return expr_new_function(expr_new_symbol(SYM_Graph), gargs, 2); +} diff --git a/src/graph/edgeadd.c b/src/graph/edgeadd.c new file mode 100644 index 00000000..bcc884c9 --- /dev/null +++ b/src/graph/edgeadd.c @@ -0,0 +1,93 @@ +/* edgeadd.c - EdgeAdd[g, e] / EdgeAdd[g, {e1, ...}]: g with the given edge(s) + * added. Any endpoint that is not already a vertex of g is added as a new vertex, + * so the result is always a valid graph. + * + * An edge spec may be DirectedEdge/UndirectedEdge or the sugar a->b (Rule) / + * a<->b (TwoWayRule); it is normalized to the corresponding edge kind. To keep + * the simple-graph invariant, a self-loop (a == b) is skipped and an edge equal + * to one already present (edge-kind-aware, symmetric for undirected) is not + * duplicated. O((V+E) * #specs) at small-graph scale. + * + * Memory (SPEC section 4): returns a freshly-built Graph; frees res. NULL on a + * non-graph first argument. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +static int spec_edge(const Expr* e, const char** kind, const Expr** a, const Expr** b) { + if (e->type != EXPR_FUNCTION || e->data.function.arg_count != 2 || + e->data.function.head->type != EXPR_SYMBOL) return 0; + const char* h = e->data.function.head->data.symbol; + if (h == SYM_DirectedEdge || h == SYM_Rule) *kind = SYM_DirectedEdge; + else if (h == SYM_UndirectedEdge || h == SYM_TwoWayRule) *kind = SYM_UndirectedEdge; + else return 0; + *a = e->data.function.args[0]; + *b = e->data.function.args[1]; + return 1; +} + +static int same_edge(const Expr* ge, const char* kind, const Expr* sa, const Expr* sb) { + if (graph_edge_kind(ge) != kind) return 0; + const Expr* a = ge->data.function.args[0]; + const Expr* b = ge->data.function.args[1]; + if (expr_eq(a, sa) && expr_eq(b, sb)) return 1; + if (kind == SYM_UndirectedEdge && expr_eq(a, sb) && expr_eq(b, sa)) return 1; + return 0; +} + +Expr* builtin_edge_add(Expr* res) { + if (res->data.function.arg_count != 2) return NULL; + const Expr* g = res->data.function.args[0]; + const Expr* spec = res->data.function.args[1]; + if (!graph_is_valid(g)) return NULL; + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + size_t n = verts->data.function.arg_count; + size_t m = edges->data.function.arg_count; + + /* Spec edges: either one edge or a List of them. */ + const Expr* const* specs; size_t ns; const Expr* one[1]; + if (graph_is_list(spec)) { specs = (const Expr* const*)spec->data.function.args; ns = spec->data.function.arg_count; } + else { one[0] = spec; specs = one; ns = 1; } + + /* Vertex pointers (borrowed): g's vertices plus any new endpoints. */ + const Expr** vptr = malloc((n + 2 * ns) * sizeof(Expr*)); + size_t nv = 0; + for (size_t i = 0; i < n; i++) vptr[nv++] = verts->data.function.args[i]; + + /* Output edges (owned copies): g's edges plus accepted new ones. */ + Expr** ec = malloc((m + ns) * sizeof(Expr*)); + size_t me = 0; + for (size_t i = 0; i < m; i++) ec[me++] = expr_copy(edges->data.function.args[i]); + + for (size_t s = 0; s < ns; s++) { + const char* kind; const Expr *a, *b; + if (!spec_edge(specs[s], &kind, &a, &b)) continue; + if (expr_eq(a, b)) continue; /* no self-loops */ + int dup = 0; + for (size_t j = 0; j < me; j++) if (same_edge(ec[j], kind, a, b)) { dup = 1; break; } + if (dup) continue; + for (int which = 0; which < 2; which++) { /* register endpoints */ + const Expr* v = which ? b : a; + int seen = 0; + for (size_t j = 0; j < nv; j++) if (expr_eq(vptr[j], v)) { seen = 1; break; } + if (!seen) vptr[nv++] = v; + } + Expr* args[2] = { expr_copy((Expr*)a), expr_copy((Expr*)b) }; + ec[me++] = expr_new_function(expr_new_symbol(kind), args, 2); + } + + Expr** vc = malloc((nv > 0 ? nv : 1) * sizeof(Expr*)); + for (size_t i = 0; i < nv; i++) vc[i] = expr_copy((Expr*)vptr[i]); + free(vptr); + + Expr* vlist = expr_new_function(expr_new_symbol(SYM_List), vc, nv); + Expr* elist = expr_new_function(expr_new_symbol(SYM_List), ec, me); + free(vc); free(ec); + Expr* gargs[2] = { vlist, elist }; + return expr_new_function(expr_new_symbol(SYM_Graph), gargs, 2); +} diff --git a/src/graph/edgebetweenness.c b/src/graph/edgebetweenness.c new file mode 100644 index 00000000..182d41e1 --- /dev/null +++ b/src/graph/edgebetweenness.c @@ -0,0 +1,108 @@ +/* edgebetweenness.c - EdgeBetweennessCentrality[g]: for each edge, the number of + * shortest paths that run along it, summed over all vertex pairs -- the edge + * analogue of BetweennessCentrality (and the score Girvan-Newman peels). + * + * The number of shortest s->t paths using directed edge a->b is sigma_sa * + * sigma_bt whenever d(s,a) + 1 + d(b,t) = d(s,t); dividing by sigma_st gives the + * fraction of shortest s-t paths through it. Summed over ordered pairs: + * eb(a->b) = sum_{s,t : d(s,a)+1+d(b,t)=d(s,t)} sigma_sa * sigma_bt / sigma_st. + * An undirected edge carries paths both ways, so both a->b and b->a are summed + * and the ordered total is halved (matching the undirected-pair convention of + * BetweennessCentrality); a directed edge takes only its own orientation. + * + * All-pairs distances/path-counts come from one BFS per source (Brandes-style + * counting); each term is num/sigma_st and the per-edge sum is reduced by the + * evaluator, so results are exact rationals. O(E * V^2). + * + * Memory (SPEC section 4): returns a fresh List; frees res. NULL on a non-graph. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include "eval.h" +#include + +static void bfs_paths(const GraphAdj* a, int s, int* d, long* sig, int* q) { + int n = a->n; + for (int i = 0; i < n; i++) { d[i] = -1; sig[i] = 0; } + int head = 0, tail = 0; + d[s] = 0; sig[s] = 1; q[tail++] = s; + while (head < tail) { + int u = q[head++]; + for (int e = 0; e < a->outdeg[u]; e++) { + int w = a->out[u][e]; + if (d[w] < 0) { d[w] = d[u] + 1; q[tail++] = w; } + if (d[w] == d[u] + 1) sig[w] += sig[u]; + } + } +} + +/* Append the terms sigma_sa*sigma_bt/sigma_st for edge direction a->b. */ +static void add_dir_terms(int n, const int* D, const long* SIG, int ai, int bi, + Expr** terms, size_t* k) { + for (int s = 0; s < n; s++) { + int dsa = D[(size_t)s * n + ai]; + if (dsa < 0) continue; + long ssa = SIG[(size_t)s * n + ai]; + for (int t = 0; t < n; t++) { + int dbt = D[(size_t)bi * n + t], dst = D[(size_t)s * n + t]; + if (dbt < 0 || dst < 0 || dsa + 1 + dbt != dst) continue; + long num = ssa * SIG[(size_t)bi * n + t]; + long den = SIG[(size_t)s * n + t]; + if (num == 0 || den == 0) continue; + Expr* pa[2] = { expr_new_integer(den), expr_new_integer(-1) }; + Expr* inv = expr_new_function(expr_new_symbol(SYM_Power), pa, 2); + Expr* ta[2] = { expr_new_integer(num), inv }; + terms[(*k)++] = expr_new_function(expr_new_symbol(SYM_Times), ta, 2); + } + } +} + +Expr* builtin_edge_betweenness_centrality(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + int n = (int)verts->data.function.arg_count; + size_t m = edges->data.function.arg_count; + + GraphAdj* a = graph_build_adj(g); + if (!a) return NULL; + + int* D = (n > 0) ? malloc((size_t)n * n * sizeof(int)) : NULL; + long* SIG = (n > 0) ? malloc((size_t)n * n * sizeof(long)) : NULL; + int* q = (n > 0) ? malloc((size_t)n * sizeof(int)) : NULL; + for (int s = 0; s < n; s++) bfs_paths(a, s, D + (size_t)s * n, SIG + (size_t)s * n, q); + free(q); + graph_adj_free(a); + + Expr** items = (m > 0) ? calloc(m, sizeof(Expr*)) : NULL; + for (size_t e = 0; e < m; e++) { + const Expr* ed = edges->data.function.args[e]; + int undirected = (graph_edge_kind(ed) == SYM_UndirectedEdge); + int ai = graph_vertex_index(verts, ed->data.function.args[0]); + int bi = graph_vertex_index(verts, ed->data.function.args[1]); + Expr** terms = (n > 0) ? calloc((size_t)2 * n * n, sizeof(Expr*)) : NULL; + size_t k = 0; + add_dir_terms(n, D, SIG, ai, bi, terms, &k); + if (undirected) add_dir_terms(n, D, SIG, bi, ai, terms, &k); + Expr* sum = (k == 0) ? expr_new_integer(0) + : expr_new_function(expr_new_symbol(SYM_Plus), terms, k); + free(terms); + if (undirected && k > 0) { /* halve the ordered-pair sum */ + Expr* ha[2] = { expr_new_integer(2), expr_new_integer(-1) }; + Expr* half = expr_new_function(expr_new_symbol(SYM_Power), ha, 2); + Expr* ma[2] = { sum, half }; + sum = expr_new_function(expr_new_symbol(SYM_Times), ma, 2); + } + items[e] = evaluate(sum); + } + free(D); free(SIG); + + Expr* out = expr_new_function(expr_new_symbol(SYM_List), items, m); + free(items); + return out; +} diff --git a/src/graph/edgeconnectivity.c b/src/graph/edgeconnectivity.c new file mode 100644 index 00000000..3cab2413 --- /dev/null +++ b/src/graph/edgeconnectivity.c @@ -0,0 +1,99 @@ +/* edgeconnectivity.c - EdgeConnectivity[g]: the minimum number of edges whose + * removal disconnects g (0 if g is already disconnected / not strongly + * connected). The edge companion to VertexConnectivity. + * + * By the max-flow/min-cut theorem the minimum s-t edge cut equals the max s-t + * flow with unit edge capacities. Global edge connectivity is then: + * undirected: fix a source s0 and take min over all t != s0 of maxflow(s0,t) + * (a standard result — one source suffices), + * directed: min over all ordered pairs (s,t), s != t. + * Each flow is Edmonds-Karp (BFS-augmenting), O(V*E^2); overall O(V^2) flows for + * the directed case, O(V) for undirected — fine for the MVP's small graphs. + * + * Memory (SPEC section 4): returns a fresh integer; frees res. NULL on a + * non-graph argument. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include +#include +#include + +/* Max s->t flow over the unit-capacity residual graph `cap` (n x n). */ +static int max_flow(const int* cap, int n, int s, int t) { + int* res = malloc((size_t)n * n * sizeof(int)); + int* parent = malloc((size_t)n * sizeof(int)); + int* q = malloc((size_t)n * sizeof(int)); + if (!res || !parent || !q) { free(res); free(parent); free(q); return 0; } + memcpy(res, cap, (size_t)n * n * sizeof(int)); + + int flow = 0; + for (;;) { + for (int i = 0; i < n; i++) parent[i] = -1; + parent[s] = s; + int head = 0, tail = 0; + q[tail++] = s; + while (head < tail && parent[t] < 0) { + int u = q[head++]; + for (int v = 0; v < n; v++) + if (parent[v] < 0 && res[(size_t)u * n + v] > 0) { parent[v] = u; q[tail++] = v; } + } + if (parent[t] < 0) break; /* no augmenting path */ + int bottleneck = INT_MAX; + for (int v = t; v != s; v = parent[v]) { + int u = parent[v]; + if (res[(size_t)u * n + v] < bottleneck) bottleneck = res[(size_t)u * n + v]; + } + for (int v = t; v != s; v = parent[v]) { + int u = parent[v]; + res[(size_t)u * n + v] -= bottleneck; + res[(size_t)v * n + u] += bottleneck; + } + flow += bottleneck; + } + free(res); free(parent); free(q); + return flow; +} + +Expr* builtin_edge_connectivity(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + int n = (int)verts->data.function.arg_count; + size_t ne = edges->data.function.arg_count; + if (n < 2) return expr_new_integer(0); + + int directed = (ne > 0); + for (size_t i = 0; i < ne; i++) + if (graph_edge_kind(edges->data.function.args[i]) != SYM_DirectedEdge) { directed = 0; break; } + + int* cap = calloc((size_t)n * n, sizeof(int)); + if (!cap) return NULL; + for (size_t k = 0; k < ne; k++) { + const Expr* e = edges->data.function.args[k]; + int a = graph_vertex_index(verts, e->data.function.args[0]); + int b = graph_vertex_index(verts, e->data.function.args[1]); + if (a < 0 || b < 0) continue; + cap[(size_t)a * n + b] += 1; + if (!directed) cap[(size_t)b * n + a] += 1; + } + + int lambda = INT_MAX; + if (directed) { + for (int s = 0; s < n && lambda > 0; s++) + for (int t = 0; t < n && lambda > 0; t++) + if (s != t) { int f = max_flow(cap, n, s, t); if (f < lambda) lambda = f; } + } else { + for (int t = 1; t < n && lambda > 0; t++) { + int f = max_flow(cap, n, 0, t); + if (f < lambda) lambda = f; + } + } + free(cap); + return expr_new_integer(lambda == INT_MAX ? 0 : lambda); +} diff --git a/src/graph/edgecontract.c b/src/graph/edgecontract.c new file mode 100644 index 00000000..790954aa --- /dev/null +++ b/src/graph/edgecontract.c @@ -0,0 +1,93 @@ +/* edgecontract.c - EdgeContract[g, e]: contract the edge e by merging its two + * endpoints into a single vertex (the first endpoint), redirecting every incident + * edge to it, deleting the resulting self-loop, and collapsing parallel edges. + * + * The edge may be given as DirectedEdge/UndirectedEdge, the sugar a->b / a<->b, + * or a two-element list {a, b}; both endpoints must be vertices of g. This is the + * edge-oriented view of vertex contraction: EdgeContract[g, {u,v}] equals + * VertexContract[g, {u,v}]. O((V+E) + E^2) at small-graph scale; edge kinds are + * preserved. + * + * Memory (SPEC section 4): returns a freshly-built Graph; frees res. NULL unless + * g is a valid graph and e names two vertices of g. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +static int same_edge(const Expr* a, const Expr* b) { + const char* ka = graph_edge_kind(a); + const char* kb = graph_edge_kind(b); + if (!ka || ka != kb) return 0; + const Expr* a0 = a->data.function.args[0]; + const Expr* a1 = a->data.function.args[1]; + const Expr* b0 = b->data.function.args[0]; + const Expr* b1 = b->data.function.args[1]; + if (expr_eq(a0, b0) && expr_eq(a1, b1)) return 1; + if (ka == SYM_UndirectedEdge && expr_eq(a0, b1) && expr_eq(a1, b0)) return 1; + return 0; +} + +/* Extract the two endpoints of an edge spec; returns 0 on failure. */ +static int spec_endpoints(const Expr* e, const Expr** a, const Expr** b) { + if (e->type != EXPR_FUNCTION || e->data.function.arg_count != 2 || + e->data.function.head->type != EXPR_SYMBOL) return 0; + const char* h = e->data.function.head->data.symbol; + if (h == SYM_DirectedEdge || h == SYM_UndirectedEdge || + h == SYM_Rule || h == SYM_TwoWayRule || h == SYM_List) { + *a = e->data.function.args[0]; + *b = e->data.function.args[1]; + return 1; + } + return 0; +} + +Expr* builtin_edge_contract(Expr* res) { + if (res->data.function.arg_count != 2) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + const Expr *ea, *eb; + if (!spec_endpoints(res->data.function.args[1], &ea, &eb)) return NULL; + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + size_t n = verts->data.function.arg_count; + size_t m = edges->data.function.arg_count; + if (graph_vertex_index(verts, ea) < 0 || graph_vertex_index(verts, eb) < 0) return NULL; + if (expr_eq(ea, eb)) return NULL; /* not an edge */ + + /* New vertices: keep everything except eb (absorbed into ea). */ + Expr** vc = malloc((n > 0 ? n : 1) * sizeof(Expr*)); + size_t nv = 0; + for (size_t i = 0; i < n; i++) { + const Expr* v = verts->data.function.args[i]; + if (expr_eq(v, eb)) continue; + vc[nv++] = expr_copy((Expr*)v); + } + + /* Remap edges (eb -> ea), drop self-loops, dedup. */ + Expr** ec = (m > 0) ? malloc(m * sizeof(Expr*)) : NULL; + size_t me = 0; + for (size_t k = 0; k < m; k++) { + const Expr* e = edges->data.function.args[k]; + const char* kind = graph_edge_kind(e); + const Expr* a = e->data.function.args[0]; + const Expr* b = e->data.function.args[1]; + const Expr* na = expr_eq(a, eb) ? ea : a; + const Expr* nb = expr_eq(b, eb) ? ea : b; + if (expr_eq(na, nb)) continue; /* self-loop */ + Expr* ne = expr_new_function(expr_new_symbol(kind), + (Expr*[]){ expr_copy((Expr*)na), expr_copy((Expr*)nb) }, 2); + int dup = 0; + for (size_t j = 0; j < me; j++) if (same_edge(ec[j], ne)) { dup = 1; break; } + if (dup) expr_free(ne); else ec[me++] = ne; + } + + Expr* vlist = expr_new_function(expr_new_symbol(SYM_List), vc, nv); + Expr* elist = expr_new_function(expr_new_symbol(SYM_List), ec, me); + free(vc); free(ec); + Expr* gargs[2] = { vlist, elist }; + return expr_new_function(expr_new_symbol(SYM_Graph), gargs, 2); +} diff --git a/src/graph/edgedelete.c b/src/graph/edgedelete.c new file mode 100644 index 00000000..9d27742b --- /dev/null +++ b/src/graph/edgedelete.c @@ -0,0 +1,81 @@ +/* edgedelete.c - EdgeDelete[g, e] / EdgeDelete[g, {e1, ...}]: g with the given + * edge (or edges) removed. All vertices are kept. + * + * An edge spec may be written as DirectedEdge/UndirectedEdge or with the sugar + * a->b (Rule) / a<->b (TwoWayRule); it is normalized to a (kind, a, b) triple + * and matched against g's edges with the usual edge-kind-aware, symmetric-for- + * undirected equality. A List second argument names several edges to delete; + * only the first matching graph edge is removed per spec occurrence. O(E * #specs) + * at small-graph scale. + * + * Memory (SPEC section 4): returns a freshly-built Graph; frees res. NULL on a + * non-graph first argument. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +/* Normalize an edge spec to (kind, a, b); returns 0 if it is not an edge. */ +static int spec_edge(const Expr* e, const char** kind, const Expr** a, const Expr** b) { + if (e->type != EXPR_FUNCTION || e->data.function.arg_count != 2 || + e->data.function.head->type != EXPR_SYMBOL) return 0; + const char* h = e->data.function.head->data.symbol; + if (h == SYM_DirectedEdge || h == SYM_Rule) *kind = SYM_DirectedEdge; + else if (h == SYM_UndirectedEdge || h == SYM_TwoWayRule) *kind = SYM_UndirectedEdge; + else return 0; + *a = e->data.function.args[0]; + *b = e->data.function.args[1]; + return 1; +} + +/* Does graph edge ge match the normalized spec (kind, sa, sb)? */ +static int edge_matches(const Expr* ge, const char* kind, const Expr* sa, const Expr* sb) { + if (graph_edge_kind(ge) != kind) return 0; + const Expr* a = ge->data.function.args[0]; + const Expr* b = ge->data.function.args[1]; + if (expr_eq(a, sa) && expr_eq(b, sb)) return 1; + if (kind == SYM_UndirectedEdge && expr_eq(a, sb) && expr_eq(b, sa)) return 1; + return 0; +} + +/* Is graph edge ge named by the delete spec (single edge or List of edges)? */ +static int in_edge_spec(const Expr* spec, const Expr* ge) { + const char* kind; const Expr *a, *b; + if (graph_is_list(spec)) { + for (size_t i = 0; i < spec->data.function.arg_count; i++) + if (spec_edge(spec->data.function.args[i], &kind, &a, &b) && + edge_matches(ge, kind, a, b)) return 1; + return 0; + } + return spec_edge(spec, &kind, &a, &b) && edge_matches(ge, kind, a, b); +} + +Expr* builtin_edge_delete(Expr* res) { + if (res->data.function.arg_count != 2) return NULL; + const Expr* g = res->data.function.args[0]; + const Expr* spec = res->data.function.args[1]; + if (!graph_is_valid(g)) return NULL; + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + size_t n = verts->data.function.arg_count; + size_t m = edges->data.function.arg_count; + + Expr** vc = malloc((n > 0 ? n : 1) * sizeof(Expr*)); + for (size_t i = 0; i < n; i++) vc[i] = expr_copy(verts->data.function.args[i]); + + Expr** ec = (m > 0) ? malloc(m * sizeof(Expr*)) : NULL; + size_t me = 0; + for (size_t k = 0; k < m; k++) { + const Expr* e = edges->data.function.args[k]; + if (!in_edge_spec(spec, e)) ec[me++] = expr_copy((Expr*)e); + } + + Expr* vlist = expr_new_function(expr_new_symbol(SYM_List), vc, n); + Expr* elist = expr_new_function(expr_new_symbol(SYM_List), ec, me); + free(vc); free(ec); + Expr* gargs[2] = { vlist, elist }; + return expr_new_function(expr_new_symbol(SYM_Graph), gargs, 2); +} diff --git a/src/graph/edgelist.c b/src/graph/edgelist.c new file mode 100644 index 00000000..f1a5e065 --- /dev/null +++ b/src/graph/edgelist.c @@ -0,0 +1,13 @@ +/* edgelist.c - EdgeList[g]: the graph's edges (canonical Directed/UndirectedEdge + * form), in order. NULL (unevaluated) if g is not a graph. + * Memory (SPEC section 4): returns a fresh copy; the evaluator frees res. */ + +#include "graph.h" +#include "expr.h" + +Expr* builtin_edge_list(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + return expr_copy(g->data.function.args[1]); +} diff --git a/src/graph/emptygraphq.c b/src/graph/emptygraphq.c new file mode 100644 index 00000000..2bf7b425 --- /dev/null +++ b/src/graph/emptygraphq.c @@ -0,0 +1,17 @@ +/* emptygraphq.c - EmptyGraphQ[g]: True iff g has no edges (an edgeless graph on + * any number of vertices, including none). O(1) -- just the edge count. + * + * Memory (SPEC section 4): returns True/False; frees res. NULL on a non-graph. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" + +Expr* builtin_empty_graph_q(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + int empty = (g->data.function.args[1]->data.function.arg_count == 0); + return expr_new_symbol(empty ? SYM_True : SYM_False); +} diff --git a/src/graph/eulerianq.c b/src/graph/eulerianq.c new file mode 100644 index 00000000..3723ff49 --- /dev/null +++ b/src/graph/eulerianq.c @@ -0,0 +1,60 @@ +/* eulerianq.c - EulerianGraphQ[g]: True iff g has an Eulerian cycle (a closed + * walk traversing every edge exactly once). + * + * Conditions: + * undirected: every vertex has even degree, and all vertices of nonzero degree + * lie in one connected component; + * directed: in-degree equals out-degree at every vertex, and all nonzero- + * degree vertices lie in one component. (When in==out everywhere, + * weak connectivity implies strong connectivity, so the undirected + * component test suffices.) + * Isolated vertices are ignored; an edgeless graph is vacuously Eulerian. + * + * O(V+E) — degree scan plus one component count over the shared adjacency. + * Memory (SPEC section 4): returns a fresh True/False symbol; frees res. False + * on a non-graph argument (matching GraphQ). + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +Expr* builtin_eulerian_graph_q(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return expr_new_symbol(SYM_False); + + const Expr* edges = g->data.function.args[1]; + size_t ne = edges->data.function.arg_count; + int directed = (ne > 0); + for (size_t i = 0; i < ne; i++) + if (graph_edge_kind(edges->data.function.args[i]) != SYM_DirectedEdge) { directed = 0; break; } + + GraphAdj* a = graph_build_adj(g); + if (!a) return expr_new_symbol(SYM_False); + int n = a->n; + + /* Degree condition. For an undirected graph a->outdeg is the (incident) + * degree; for a directed graph we need in-degree == out-degree. */ + int degree_ok = 1; + for (int i = 0; i < n; i++) { + if (directed) { if (a->indeg[i] != a->outdeg[i]) { degree_ok = 0; break; } } + else { if (a->outdeg[i] % 2 != 0) { degree_ok = 0; break; } } + } + + int eulerian = 0; + if (degree_ok) { + /* Connectivity over the nonzero-degree vertices only. */ + char* removed = (n > 0) ? calloc((size_t)n, 1) : NULL; + for (int i = 0; i < n; i++) + if (a->outdeg[i] + a->indeg[i] == 0) removed[i] = 1; /* isolated */ + int active = 0; + int comps = graph_count_components(a, removed, &active); + eulerian = (active == 0) ? 1 : (comps == 1); /* edgeless is vacuous */ + free(removed); + } + + graph_adj_free(a); + return expr_new_symbol(eulerian ? SYM_True : SYM_False); +} diff --git a/src/graph/findclique.c b/src/graph/findclique.c new file mode 100644 index 00000000..a48ad33d --- /dev/null +++ b/src/graph/findclique.c @@ -0,0 +1,92 @@ +/* findclique.c - FindClique[g]: a largest clique (a set of pairwise-adjacent + * vertices) as a list containing one clique -- Wolfram's shape, e.g. + * {{1, 2, 3, 4}} for K4 -- or {} for a graph with no vertices. + * + * Recursive clique expansion (a max-clique specialisation of Bron-Kerbosch): grow + * a clique R over a candidate set P of vertices adjacent to all of R, considering + * only candidates after the last-added one so each clique is reached once. The + * best clique seen is kept; the branch `|R| + |remaining candidates| <= |best|` + * is pruned, which keeps the exponential worst case tiny on real graphs. Edge + * direction is ignored (the clique is defined on the underlying undirected + * graph). Deterministic: candidates in vertex order, the first maximum kept. + * + * Memory (SPEC section 4): returns a fresh List of one List; frees res. NULL on + * a non-graph argument. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include +#include + +typedef struct { + const char* adj; + int n; + int* best; + int bestlen; +} CliqueEnv; + +static void expand(CliqueEnv* e, int* R, int rlen, int* P, int plen) { + if (rlen > e->bestlen) { memcpy(e->best, R, (size_t)rlen * sizeof(int)); e->bestlen = rlen; } + for (int i = 0; i < plen; i++) { + if (rlen + (plen - i) <= e->bestlen) break; /* can't beat best */ + int v = P[i]; + R[rlen] = v; + int* newP = (plen - i > 1) ? malloc((size_t)(plen - i - 1) * sizeof(int)) : NULL; + int np = 0; + for (int j = i + 1; j < plen; j++) + if (e->adj[(size_t)v * e->n + P[j]]) newP[np++] = P[j]; + expand(e, R, rlen + 1, newP, np); + free(newP); + } +} + +Expr* builtin_find_clique(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + int n = (int)verts->data.function.arg_count; + size_t ne = edges->data.function.arg_count; + + char* adj = (n > 0) ? calloc((size_t)n * (size_t)n, 1) : NULL; + for (size_t k = 0; k < ne; k++) { + const Expr* ed = edges->data.function.args[k]; + int ia = graph_vertex_index(verts, ed->data.function.args[0]); + int ib = graph_vertex_index(verts, ed->data.function.args[1]); + if (ia < 0 || ib < 0 || ia == ib) continue; + adj[(size_t)ia * n + ib] = adj[(size_t)ib * n + ia] = 1; + } + + CliqueEnv e = { adj, n, (n > 0) ? malloc((size_t)n * sizeof(int)) : NULL, 0 }; + if (n > 0) { + int* R = malloc((size_t)n * sizeof(int)); + int* P = malloc((size_t)n * sizeof(int)); + for (int i = 0; i < n; i++) P[i] = i; + expand(&e, R, 0, P, n); + free(R); free(P); + } + free(adj); + + Expr* out; + if (e.bestlen > 0) { + /* best[] holds indices in discovery order; emit in canonical order. */ + char* inset = calloc((size_t)n, 1); + for (int i = 0; i < e.bestlen; i++) inset[e.best[i]] = 1; + Expr** vs = calloc((size_t)e.bestlen, sizeof(Expr*)); + int idx = 0; + for (int v = 0; v < n; v++) if (inset[v]) vs[idx++] = expr_copy(verts->data.function.args[v]); + free(inset); + Expr* clique = expr_new_function(expr_new_symbol(SYM_List), vs, (size_t)e.bestlen); + free(vs); + Expr* one[1] = { clique }; + out = expr_new_function(expr_new_symbol(SYM_List), one, 1); + } else { + out = expr_new_function(expr_new_symbol(SYM_List), NULL, 0); + } + free(e.best); + return out; +} diff --git a/src/graph/findcycle.c b/src/graph/findcycle.c new file mode 100644 index 00000000..ab880bc9 --- /dev/null +++ b/src/graph/findcycle.c @@ -0,0 +1,118 @@ +/* findcycle.c - FindCycle[g]: a cycle in g as a list containing one cycle, that + * cycle being a list of its edges (Wolfram's shape, e.g. {{1<->2, 2<->3, + * 3<->1}}), or {} when g is acyclic. + * + * Depth-first search with back-edge detection, O(V+E): + * directed g -> a gray (on-stack) target closes a directed cycle; + * undirected g -> a visited neighbour that is not the DFS parent closes one + * (undirected DFS has only tree and back edges, so any such + * neighbour is a genuine ancestor). + * The cycle is rebuilt by walking DFS-tree parent pointers from the back edge's + * tail up to its head. The first cycle found (deterministic: sources and + * neighbours in canonical order) is returned -- not necessarily the shortest. + * + * Edge kinds mirror the source graph: DirectedEdge for a fully directed graph, + * UndirectedEdge otherwise. A cycle needs >= 3 vertices in the undirected case, + * automatic under the simple-graph invariant (no self-loops / parallel edges). + * + * Memory (SPEC section 4): returns a fresh List; frees res. NULL on a non-graph. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +typedef struct { + const GraphAdj* a; + int* state; /* directed: 0 white / 1 gray / 2 black */ + char* visited; /* undirected */ + int* parent; /* DFS-tree parent (vertex index), -1 at a root */ + int* cyc; /* reconstructed cycle vertices, length cyclen */ + int* tmp; /* scratch for reconstruction */ + int cyclen; /* 0 while no cycle found */ +} CycEnv; + +/* Record the cycle head..tail (ancestor u down to back-edge tail v). */ +static void cyc_record(CycEnv* e, int v, int u) { + int len = 0, w = v; + for (;;) { e->tmp[len++] = w; if (w == u) break; w = e->parent[w]; } + for (int i = 0; i < len; i++) e->cyc[i] = e->tmp[len - 1 - i]; /* u..v */ + e->cyclen = len; +} + +static int dfs_dir(CycEnv* e, int v) { + e->state[v] = 1; + for (int j = 0; j < e->a->outdeg[v]; j++) { + int u = e->a->out[v][j]; + if (e->state[u] == 0) { e->parent[u] = v; if (dfs_dir(e, u)) return 1; } + else if (e->state[u] == 1) { cyc_record(e, v, u); return 1; } + } + e->state[v] = 2; + return 0; +} + +static int dfs_undir(CycEnv* e, int v, int par) { + e->visited[v] = 1; e->parent[v] = par; + for (int j = 0; j < e->a->outdeg[v]; j++) { + int u = e->a->out[v][j]; + if (!e->visited[u]) { if (dfs_undir(e, u, v)) return 1; } + else if (u != par) { cyc_record(e, v, u); return 1; } + } + return 0; +} + +Expr* builtin_find_cycle(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + int n = (int)verts->data.function.arg_count; + size_t ne = edges->data.function.arg_count; + + int directed = (ne > 0); + for (size_t i = 0; i < ne; i++) + if (graph_edge_kind(edges->data.function.args[i]) != SYM_DirectedEdge) { directed = 0; break; } + + GraphAdj* a = graph_build_adj(g); + if (!a) return NULL; + + CycEnv e; + e.a = a; + e.state = calloc((size_t)(n > 0 ? n : 1), sizeof(int)); + e.visited = calloc((size_t)(n > 0 ? n : 1), 1); + e.parent = malloc((size_t)(n > 0 ? n : 1) * sizeof(int)); + e.cyc = malloc((size_t)(n > 0 ? n : 1) * sizeof(int)); + e.tmp = malloc((size_t)(n > 0 ? n : 1) * sizeof(int)); + e.cyclen = 0; + for (int i = 0; i < n; i++) e.parent[i] = -1; + + for (int s = 0; s < n && e.cyclen == 0; s++) { + if (directed) { if (e.state[s] == 0) dfs_dir(&e, s); } + else { if (!e.visited[s]) dfs_undir(&e, s, -1); } + } + + Expr* out; + if (e.cyclen > 0) { + const char* ekind = directed ? SYM_DirectedEdge : SYM_UndirectedEdge; + Expr** cedges = calloc((size_t)e.cyclen, sizeof(Expr*)); + for (int i = 0; i < e.cyclen; i++) { + int from = e.cyc[i], to = e.cyc[(i + 1) % e.cyclen]; + Expr* args[2] = { expr_copy(verts->data.function.args[from]), + expr_copy(verts->data.function.args[to]) }; + cedges[i] = expr_new_function(expr_new_symbol(ekind), args, 2); + } + Expr* cycle = expr_new_function(expr_new_symbol(SYM_List), cedges, (size_t)e.cyclen); + free(cedges); + Expr* one[1] = { cycle }; + out = expr_new_function(expr_new_symbol(SYM_List), one, 1); + } else { + out = expr_new_function(expr_new_symbol(SYM_List), NULL, 0); + } + + free(e.state); free(e.visited); free(e.parent); free(e.cyc); free(e.tmp); + graph_adj_free(a); + return out; +} diff --git a/src/graph/finddominatingset.c b/src/graph/finddominatingset.c new file mode 100644 index 00000000..6a8ed482 --- /dev/null +++ b/src/graph/finddominatingset.c @@ -0,0 +1,72 @@ +/* finddominatingset.c - FindDominatingSet[g]: a minimum dominating set of g -- a + * smallest set S of vertices such that every vertex is in S or adjacent to a + * vertex of S -- returned as a vertex list. + * + * Each vertex gets a closed-neighbourhood bitmask (itself plus its neighbours); + * a set dominates iff the OR of its masks is all-ones. We search by increasing + * size k = 1, 2, ... over the C(n,k) subsets and return the first dominating one, + * so the result is a minimum dominating set. Edge direction is ignored. The + * exponential subset search is bounded to modest n (DOM_MAX_N below, which also + * keeps the vertex bitmasks within one 64-bit word); the call is left + * unevaluated for larger graphs. + * + * A star is dominated by its centre; K_n by any single vertex; an edgeless graph + * needs all its vertices. Memory (SPEC section 4): returns a fresh List; frees + * res. NULL on a non-graph or an oversized graph. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +#define DOM_MAX_N 26 + +Expr* builtin_find_dominating_set(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + int n = (int)verts->data.function.arg_count; + if (n == 0) return expr_new_function(expr_new_symbol(SYM_List), NULL, 0); + if (n > DOM_MAX_N) return NULL; /* too large to search */ + + /* Closed-neighbourhood masks (vertex i plus its undirected neighbours). */ + unsigned long long* mask = calloc((size_t)n, sizeof(unsigned long long)); + for (int i = 0; i < n; i++) mask[i] = 1ULL << i; + for (size_t k = 0; k < edges->data.function.arg_count; k++) { + const Expr* e = edges->data.function.args[k]; + int a = graph_vertex_index(verts, e->data.function.args[0]); + int b = graph_vertex_index(verts, e->data.function.args[1]); + if (a < 0 || b < 0 || a == b) continue; + mask[a] |= 1ULL << b; mask[b] |= 1ULL << a; + } + unsigned long long full = (1ULL << n) - 1; /* n <= DOM_MAX_N, so no overflow */ + + /* Search subsets by increasing size; first dominating set is minimum. */ + int* idx = malloc((size_t)n * sizeof(int)); + int* best = NULL; int bestlen = -1; + for (int k = 1; k <= n && bestlen < 0; k++) { + for (int i = 0; i < k; i++) idx[i] = i; + for (;;) { + unsigned long long cover = 0; + for (int i = 0; i < k; i++) cover |= mask[idx[i]]; + if (cover == full) { best = malloc((size_t)k * sizeof(int)); for (int i = 0; i < k; i++) best[i] = idx[i]; bestlen = k; break; } + int p = k - 1; + while (p >= 0 && idx[p] == n - k + p) p--; + if (p < 0) break; + idx[p]++; + for (int i = p + 1; i < k; i++) idx[i] = idx[i - 1] + 1; + } + } + free(idx); free(mask); + + Expr** items = (bestlen > 0) ? calloc((size_t)bestlen, sizeof(Expr*)) : NULL; + for (int i = 0; i < bestlen; i++) items[i] = expr_copy(verts->data.function.args[best[i]]); + free(best); + Expr* out = expr_new_function(expr_new_symbol(SYM_List), items, (size_t)(bestlen > 0 ? bestlen : 0)); + free(items); + return out; +} diff --git a/src/graph/findedgecover.c b/src/graph/findedgecover.c new file mode 100644 index 00000000..37c0c3b2 --- /dev/null +++ b/src/graph/findedgecover.c @@ -0,0 +1,99 @@ +/* findedgecover.c - FindEdgeCover[g]: a minimum edge cover -- a smallest set of + * edges such that every vertex is incident to at least one of them -- returned as + * a list of edges. + * + * By Gallai's construction a minimum edge cover has size n - (maximum matching), + * and is obtained by taking a maximum matching M and adding, for each vertex not + * covered by M, one edge incident to it. (Unmatched vertices are pairwise + * non-adjacent -- else M would not be maximum -- so each added edge covers + * exactly one new vertex, giving the minimum.) A maximum matching is found by the + * same branch-and-bound as FindIndependentEdgeSet. + * + * An edge cover exists iff g has no isolated vertex; when one does, {} is returned + * (no cover). Edge direction is irrelevant. Memory (SPEC section 4): returns a + * fresh List; frees res. NULL on a non-graph. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include +#include + +typedef struct { + const int* ea; const int* eb; int m; + char* used; int* cur; int* best; int bestlen; +} MEnv; + +static void expand(MEnv* e, int i, int count) { + if (count > e->bestlen) { memcpy(e->best, e->cur, (size_t)count * sizeof(int)); e->bestlen = count; } + if (i == e->m || count + (e->m - i) <= e->bestlen) return; + int u = e->ea[i], v = e->eb[i]; + if (!e->used[u] && !e->used[v]) { + e->used[u] = e->used[v] = 1; e->cur[count] = i; + expand(e, i + 1, count + 1); + e->used[u] = e->used[v] = 0; + } + expand(e, i + 1, count); +} + +Expr* builtin_find_edge_cover(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + int n = (int)verts->data.function.arg_count; + int m = (int)edges->data.function.arg_count; + Expr* empty = expr_new_function(expr_new_symbol(SYM_List), NULL, 0); + if (n == 0) return empty; + + int* ea = malloc((size_t)(m > 0 ? m : 1) * sizeof(int)); + int* eb = malloc((size_t)(m > 0 ? m : 1) * sizeof(int)); + int* deg = calloc((size_t)n, sizeof(int)); + for (int k = 0; k < m; k++) { + const Expr* e = edges->data.function.args[k]; + ea[k] = graph_vertex_index(verts, e->data.function.args[0]); + eb[k] = graph_vertex_index(verts, e->data.function.args[1]); + if (ea[k] >= 0) deg[ea[k]]++; + if (eb[k] >= 0) deg[eb[k]]++; + } + for (int i = 0; i < n; i++) if (deg[i] == 0) { /* isolated -> no cover */ + free(ea); free(eb); free(deg); return empty; + } + free(deg); + + /* Maximum matching. */ + MEnv env; env.ea = ea; env.eb = eb; env.m = m; + env.used = calloc((size_t)n, 1); + env.cur = malloc((size_t)(m > 0 ? m : 1) * sizeof(int)); + env.best = malloc((size_t)(m > 0 ? m : 1) * sizeof(int)); + env.bestlen = 0; + if (m > 0) expand(&env, 0, 0); + + /* Cover = matching edges, then one incident edge per uncovered vertex. */ + char* chosen = calloc((size_t)(m > 0 ? m : 1), 1); + char* covered = calloc((size_t)n, 1); + for (int i = 0; i < env.bestlen; i++) { + int k = env.best[i]; chosen[k] = 1; + covered[ea[k]] = covered[eb[k]] = 1; + } + for (int v = 0; v < n; v++) { + if (covered[v]) continue; + for (int k = 0; k < m; k++) + if (ea[k] == v || eb[k] == v) { chosen[k] = 1; covered[ea[k]] = covered[eb[k]] = 1; break; } + } + + size_t cnt = 0; + for (int k = 0; k < m; k++) cnt += chosen[k]; + Expr** items = (cnt > 0) ? calloc(cnt, sizeof(Expr*)) : NULL; + size_t idx = 0; + for (int k = 0; k < m; k++) if (chosen[k]) items[idx++] = expr_copy(edges->data.function.args[k]); + free(ea); free(eb); free(env.used); free(env.cur); free(env.best); free(chosen); free(covered); + expr_free(empty); + + Expr* out = expr_new_function(expr_new_symbol(SYM_List), items, cnt); + free(items); + return out; +} diff --git a/src/graph/findeuler.c b/src/graph/findeuler.c new file mode 100644 index 00000000..78007704 --- /dev/null +++ b/src/graph/findeuler.c @@ -0,0 +1,106 @@ +/* findeuler.c - FindEulerianCycle[g]: an Eulerian cycle (a closed walk using + * every edge exactly once) as a vertex list {v0, v1, ..., v0}, or {} if none + * exists. The constructive companion to EulerianGraphQ. + * + * Hierholzer's algorithm, O(V+E): walk unused edges onto a stack, splicing in + * sub-tours as dead ends are hit; the reversed pop-order is the Euler tour. It + * doubles as the existence test — if the tour fails to use all ne edges (odd + * degree / in!=out / disconnected), the graph is not Eulerian and we return {}. + * Works for directed (follow out-edges) and undirected (each edge usable once + * from either end). + * + * Memory (SPEC section 4): returns a fresh List; frees res. NULL on a non-graph. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +Expr* builtin_find_eulerian_cycle(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + int n = (int)verts->data.function.arg_count; + int ne = (int)edges->data.function.arg_count; + Expr* empty = expr_new_function(expr_new_symbol(SYM_List), NULL, 0); + if (ne == 0) return empty; /* no edges -> no cycle */ + + int directed = 1; + for (int i = 0; i < ne; i++) + if (graph_edge_kind(edges->data.function.args[i]) != SYM_DirectedEdge) { directed = 0; break; } + + /* Incidence lists with edge ids (each undirected edge appears from both + * ends but shares one id, so it is walked at most once). */ + int* deg = calloc((size_t)(n > 0 ? n : 1), sizeof(int)); + int ia_b, ib_b; + for (int k = 0; k < ne; k++) { + const Expr* e = edges->data.function.args[k]; + ia_b = graph_vertex_index(verts, e->data.function.args[0]); + ib_b = graph_vertex_index(verts, e->data.function.args[1]); + deg[ia_b]++; if (!directed) deg[ib_b]++; + } + int** nbr = calloc((size_t)(n > 0 ? n : 1), sizeof(int*)); + int** eid = calloc((size_t)(n > 0 ? n : 1), sizeof(int*)); + int* pos = calloc((size_t)(n > 0 ? n : 1), sizeof(int)); + for (int v = 0; v < n; v++) { + nbr[v] = (deg[v] > 0) ? malloc((size_t)deg[v] * sizeof(int)) : NULL; + eid[v] = (deg[v] > 0) ? malloc((size_t)deg[v] * sizeof(int)) : NULL; + } + for (int k = 0; k < ne; k++) { + const Expr* e = edges->data.function.args[k]; + int a = graph_vertex_index(verts, e->data.function.args[0]); + int b = graph_vertex_index(verts, e->data.function.args[1]); + nbr[a][pos[a]] = b; eid[a][pos[a]++] = k; + if (!directed) { nbr[b][pos[b]] = a; eid[b][pos[b]++] = k; } + } + + char* used = calloc((size_t)ne, 1); + int* cursor = calloc((size_t)(n > 0 ? n : 1), sizeof(int)); + int start = 0; + for (int v = 0; v < n; v++) if (deg[v] > 0) { start = v; break; } + + int* stack = malloc((size_t)(ne + 1) * sizeof(int)); + int* circuit = malloc((size_t)(ne + 1) * sizeof(int)); + int sp = 0, cp = 0; + stack[sp++] = start; + while (sp > 0) { + int v = stack[sp - 1]; + while (cursor[v] < deg[v] && used[eid[v][cursor[v]]]) cursor[v]++; + if (cursor[v] < deg[v]) { + int idx = cursor[v]++; + used[eid[v][idx]] = 1; + stack[sp++] = nbr[v][idx]; + } else { + circuit[cp++] = v; + sp--; + } + } + + /* An Eulerian CYCLE uses every edge exactly once AND is closed. Requiring + * both distinguishes it from an Eulerian path (all edges, open walk) and + * from a traversal that stalled before covering the graph. */ + int nused = 0; + for (int k = 0; k < ne; k++) nused += used[k]; + int closed = (cp >= 2 && circuit[0] == circuit[cp - 1]); + + Expr* out; + if (nused == ne && cp == ne + 1 && closed) { + Expr** items = calloc((size_t)cp, sizeof(Expr*)); + for (int i = 0; i < cp; i++) /* circuit is reversed */ + items[i] = expr_copy((Expr*)verts->data.function.args[circuit[cp - 1 - i]]); + out = expr_new_function(expr_new_symbol(SYM_List), items, (size_t)cp); + free(items); + expr_free(empty); + } else { + out = empty; /* not Eulerian */ + } + + for (int v = 0; v < n; v++) { free(nbr[v]); free(eid[v]); } + free(nbr); free(eid); free(pos); free(deg); free(used); free(cursor); + free(stack); free(circuit); + return out; +} diff --git a/src/graph/findhamilton.c b/src/graph/findhamilton.c new file mode 100644 index 00000000..47a2ec95 --- /dev/null +++ b/src/graph/findhamilton.c @@ -0,0 +1,85 @@ +/* findhamilton.c - FindHamiltonianCycle[g]: a Hamiltonian cycle (a closed walk + * visiting every vertex exactly once) as a vertex list {v0, v1, ..., v0}, or {} + * if none exists. The constructive companion to the Eulerian-cycle finder. + * + * Depth-first backtracking with visited-set pruning, O(V!) worst case but tiny + * in practice thanks to cheap necessary-condition prunes: any vertex missing an + * out- or in-neighbour cannot lie on a cycle, so such graphs short-circuit to + * {}. The search fixes the start at vertex 0 -- a Hamiltonian cycle, if it + * exists, passes through every vertex, so this loses no generality and makes the + * result deterministic. Successors come from GraphAdj.out[], which already + * encodes edge direction (an undirected edge appears in both endpoints' out[]), + * so the same code handles directed and undirected graphs. + * + * A Hamiltonian cycle needs at least 3 distinct vertices; with the graph's + * simple-graph invariant (no self-loops, no parallel edges) every such cycle + * automatically uses distinct edges, so no per-edge bookkeeping is required. + * + * Memory (SPEC section 4): returns a fresh List; frees res. NULL on a non-graph. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +/* Try to extend the partial path path[0..depth-1] into a full Hamiltonian cycle + * rooted at `start`. Returns 1 and leaves path[0..n-1] filled on success. */ +static int ham_extend(const GraphAdj* a, int* path, char* visited, + int depth, int start) { + int last = path[depth - 1]; + if (depth == a->n) { /* all vertices used: close it? */ + for (int j = 0; j < a->outdeg[last]; j++) + if (a->out[last][j] == start) return 1; + return 0; + } + for (int j = 0; j < a->outdeg[last]; j++) { + int u = a->out[last][j]; + if (!visited[u]) { + visited[u] = 1; + path[depth] = u; + if (ham_extend(a, path, visited, depth + 1, start)) return 1; + visited[u] = 0; + } + } + return 0; +} + +Expr* builtin_find_hamiltonian_cycle(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + + GraphAdj* a = graph_build_adj(g); + if (!a) return NULL; + int n = a->n; + Expr* empty = expr_new_function(expr_new_symbol(SYM_List), NULL, 0); + + /* Fewer than 3 vertices, or any vertex lacking an out/in neighbour, rules + * out a cycle immediately. */ + int impossible = (n < 3); + for (int i = 0; i < n && !impossible; i++) + if (a->outdeg[i] == 0 || a->indeg[i] == 0) impossible = 1; + if (impossible) { graph_adj_free(a); return empty; } + + int* path = malloc((size_t)n * sizeof(int)); + char* visited = calloc((size_t)n, 1); + path[0] = 0; visited[0] = 1; + + Expr* out; + if (ham_extend(a, path, visited, 1, 0)) { + Expr** items = calloc((size_t)(n + 1), sizeof(Expr*)); + for (int i = 0; i < n; i++) + items[i] = expr_copy((Expr*)a->verts->data.function.args[path[i]]); + items[n] = expr_copy((Expr*)a->verts->data.function.args[path[0]]); /* close */ + out = expr_new_function(expr_new_symbol(SYM_List), items, (size_t)(n + 1)); + free(items); + expr_free(empty); + } else { + out = empty; + } + + free(path); free(visited); + graph_adj_free(a); + return out; +} diff --git a/src/graph/findhamiltonpath.c b/src/graph/findhamiltonpath.c new file mode 100644 index 00000000..7c0ec462 --- /dev/null +++ b/src/graph/findhamiltonpath.c @@ -0,0 +1,74 @@ +/* findhamiltonpath.c - FindHamiltonianPath[g]: a Hamiltonian path (a walk + * visiting every vertex exactly once, not necessarily returning to the start) + * as a vertex list {v0, v1, ..., v_{n-1}}, or {} if none exists. + * + * Depth-first backtracking with visited-set pruning. Unlike a Hamiltonian + * *cycle* -- where any vertex can serve as the root because the tour is closed + * -- a path's endpoints are free, so the search is retried from every start + * vertex until one yields a full-length path. Successors come from + * GraphAdj.out[], which encodes edge direction (an undirected edge sits in both + * endpoints' out[]), so directed and undirected graphs share one code path. + * + * O(V!) worst case, tiny in practice on gallery-sized graphs. Deterministic: + * starts are tried in vertex order, neighbours in adjacency order. + * + * Memory (SPEC section 4): returns a fresh List; frees res. NULL on a non-graph. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +/* Extend path[0..depth-1]; returns 1 with path[0..n-1] filled on success. */ +static int hp_extend(const GraphAdj* a, int* path, char* visited, int depth) { + if (depth == a->n) return 1; + int last = path[depth - 1]; + for (int j = 0; j < a->outdeg[last]; j++) { + int u = a->out[last][j]; + if (!visited[u]) { + visited[u] = 1; + path[depth] = u; + if (hp_extend(a, path, visited, depth + 1)) return 1; + visited[u] = 0; + } + } + return 0; +} + +Expr* builtin_find_hamiltonian_path(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + + GraphAdj* a = graph_build_adj(g); + if (!a) return NULL; + int n = a->n; + + Expr* out = NULL; + if (n == 0) { + out = expr_new_function(expr_new_symbol(SYM_List), NULL, 0); + } else { + int* path = malloc((size_t)n * sizeof(int)); + char* visited = calloc((size_t)n, 1); + int found = 0; + for (int s = 0; s < n && !found; s++) { + for (int i = 0; i < n; i++) visited[i] = 0; + path[0] = s; visited[s] = 1; + found = hp_extend(a, path, visited, 1); + } + if (found) { + Expr** items = calloc((size_t)n, sizeof(Expr*)); + for (int i = 0; i < n; i++) + items[i] = expr_copy((Expr*)a->verts->data.function.args[path[i]]); + out = expr_new_function(expr_new_symbol(SYM_List), items, (size_t)n); + free(items); + } else { + out = expr_new_function(expr_new_symbol(SYM_List), NULL, 0); + } + free(path); free(visited); + } + + graph_adj_free(a); + return out; +} diff --git a/src/graph/findindependent.c b/src/graph/findindependent.c new file mode 100644 index 00000000..dfaddac9 --- /dev/null +++ b/src/graph/findindependent.c @@ -0,0 +1,96 @@ +/* findindependent.c - FindIndependentVertexSet[g]: a largest independent vertex + * set (a set of pairwise non-adjacent vertices) as a list containing one set -- + * Wolfram's shape, e.g. {{2, 3, 4, 5}} for a star's leaves -- or {} when g has + * no vertices. + * + * A maximum independent set of g is a maximum clique of the complement of g, so + * we run the same branch-and-bound clique expansion (as in findclique.c) over + * the complemented adjacency: candidates are the vertices *non-adjacent* to the + * current set. Grow a set R over candidates P, keep the best, and prune the + * branch when |R| + |remaining| <= |best|. Edge direction is ignored; + * deterministic (candidates in vertex order, first maximum kept). Exponential + * worst case, tiny on real graphs. + * + * Memory (SPEC section 4): returns a fresh List of one List; frees res. NULL on + * a non-graph argument. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include +#include + +typedef struct { + const char* nadj; /* complement adjacency: nadj[i*n+j] = i,j non-adjacent */ + int n; + int* best; + int bestlen; +} IndepEnv; + +static void expand(IndepEnv* e, int* R, int rlen, int* P, int plen) { + if (rlen > e->bestlen) { memcpy(e->best, R, (size_t)rlen * sizeof(int)); e->bestlen = rlen; } + for (int i = 0; i < plen; i++) { + if (rlen + (plen - i) <= e->bestlen) break; + int v = P[i]; + R[rlen] = v; + int* newP = (plen - i > 1) ? malloc((size_t)(plen - i - 1) * sizeof(int)) : NULL; + int np = 0; + for (int j = i + 1; j < plen; j++) + if (e->nadj[(size_t)v * e->n + P[j]]) newP[np++] = P[j]; + expand(e, R, rlen + 1, newP, np); + free(newP); + } +} + +Expr* builtin_find_independent_vertex_set(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + int n = (int)verts->data.function.arg_count; + size_t ne = edges->data.function.arg_count; + + /* Complement adjacency: start all-connected (off diagonal), clear real edges. */ + char* nadj = (n > 0) ? malloc((size_t)n * (size_t)n) : NULL; + for (int i = 0; i < n; i++) + for (int j = 0; j < n; j++) + nadj[(size_t)i * n + j] = (i != j); + for (size_t k = 0; k < ne; k++) { + const Expr* ed = edges->data.function.args[k]; + int ia = graph_vertex_index(verts, ed->data.function.args[0]); + int ib = graph_vertex_index(verts, ed->data.function.args[1]); + if (ia < 0 || ib < 0 || ia == ib) continue; + nadj[(size_t)ia * n + ib] = nadj[(size_t)ib * n + ia] = 0; + } + + IndepEnv e = { nadj, n, (n > 0) ? malloc((size_t)n * sizeof(int)) : NULL, 0 }; + if (n > 0) { + int* R = malloc((size_t)n * sizeof(int)); + int* P = malloc((size_t)n * sizeof(int)); + for (int i = 0; i < n; i++) P[i] = i; + expand(&e, R, 0, P, n); + free(R); free(P); + } + free(nadj); + + Expr* out; + if (e.bestlen > 0) { + char* inset = calloc((size_t)n, 1); + for (int i = 0; i < e.bestlen; i++) inset[e.best[i]] = 1; + Expr** vs = calloc((size_t)e.bestlen, sizeof(Expr*)); + int idx = 0; + for (int v = 0; v < n; v++) if (inset[v]) vs[idx++] = expr_copy(verts->data.function.args[v]); + free(inset); + Expr* set = expr_new_function(expr_new_symbol(SYM_List), vs, (size_t)e.bestlen); + free(vs); + Expr* one[1] = { set }; + out = expr_new_function(expr_new_symbol(SYM_List), one, 1); + } else { + out = expr_new_function(expr_new_symbol(SYM_List), NULL, 0); + } + free(e.best); + return out; +} diff --git a/src/graph/findmatching.c b/src/graph/findmatching.c new file mode 100644 index 00000000..680169b4 --- /dev/null +++ b/src/graph/findmatching.c @@ -0,0 +1,77 @@ +/* findmatching.c - FindIndependentEdgeSet[g]: a maximum matching of g -- a + * largest set of edges no two of which share a vertex -- returned as a list of + * edges. + * + * Depth-first branch-and-bound over the edges: each edge is either taken (if both + * endpoints are still free) or skipped, tracking the best matching found and + * pruning when the current size plus the untried edges cannot beat it. Edge + * direction is irrelevant to independence (two edges conflict iff they share an + * endpoint). Exponential worst case, tiny on real graphs; deterministic (edges in + * order, first maximum kept). + * + * K_{2k} and even cycles/paths yield perfect/near-perfect matchings; a star has a + * maximum matching of size 1. Memory (SPEC section 4): returns a fresh List; + * frees res. NULL on a non-graph. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include +#include + +typedef struct { + const int* ea; const int* eb; /* edge endpoint vertex indices */ + int m; + char* used; /* vertex occupied by the current matching */ + int* cur; int curlen; /* current matching (edge indices) */ + int* best; int bestlen; +} MatchEnv; + +static void expand(MatchEnv* e, int i, int count) { + if (count > e->bestlen) { memcpy(e->best, e->cur, (size_t)count * sizeof(int)); e->bestlen = count; } + if (i == e->m || count + (e->m - i) <= e->bestlen) return; /* done / bound */ + int u = e->ea[i], v = e->eb[i]; + if (!e->used[u] && !e->used[v]) { /* take edge i */ + e->used[u] = e->used[v] = 1; + e->cur[count] = i; + expand(e, i + 1, count + 1); + e->used[u] = e->used[v] = 0; + } + expand(e, i + 1, count); /* skip edge i */ +} + +Expr* builtin_find_independent_edge_set(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + int n = (int)verts->data.function.arg_count; + int m = (int)edges->data.function.arg_count; + + int* ea = (m > 0) ? malloc((size_t)m * sizeof(int)) : NULL; + int* eb = (m > 0) ? malloc((size_t)m * sizeof(int)) : NULL; + for (int k = 0; k < m; k++) { + const Expr* e = edges->data.function.args[k]; + ea[k] = graph_vertex_index(verts, e->data.function.args[0]); + eb[k] = graph_vertex_index(verts, e->data.function.args[1]); + } + + MatchEnv env; + env.ea = ea; env.eb = eb; env.m = m; + env.used = (n > 0) ? calloc((size_t)n, 1) : NULL; + env.cur = (m > 0) ? malloc((size_t)m * sizeof(int)) : NULL; + env.best = (m > 0) ? malloc((size_t)m * sizeof(int)) : NULL; + env.curlen = env.bestlen = 0; + if (m > 0) expand(&env, 0, 0); + + Expr** items = (env.bestlen > 0) ? calloc((size_t)env.bestlen, sizeof(Expr*)) : NULL; + for (int i = 0; i < env.bestlen; i++) + items[i] = expr_copy(edges->data.function.args[env.best[i]]); + Expr* out = expr_new_function(expr_new_symbol(SYM_List), items, (size_t)env.bestlen); + + free(ea); free(eb); free(env.used); free(env.cur); free(env.best); free(items); + return out; +} diff --git a/src/graph/findvertexcoloring.c b/src/graph/findvertexcoloring.c new file mode 100644 index 00000000..88c83f87 --- /dev/null +++ b/src/graph/findvertexcoloring.c @@ -0,0 +1,71 @@ +/* findvertexcoloring.c - FindVertexColoring[g]: a proper vertex colouring of g + * that uses as few colours as possible (the chromatic number), returned as a list + * of colour indices 1..chi, one per vertex in vertex order. + * + * Tries k = 1, 2, ... colours and, for each, backtracks to colour the vertices so + * that adjacent ones differ; the first feasible k is the chromatic number and its + * colouring is returned. A symmetry cut (a vertex may open at most one brand-new + * colour) keeps the search small. Edge direction is ignored. Adjacent vertices + * always receive different colours; the number of distinct colours equals + * ChromaticNumber[g]. + * + * Memory (SPEC section 4): returns a fresh List; frees res. NULL on a non-graph. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +typedef struct { const char* adj; int n; int k; int* color; } ColEnv; + +static int color_from(ColEnv* e, int v, int maxUsed) { + if (v == e->n) return 1; + int limit = (maxUsed + 1 < e->k) ? maxUsed + 1 : e->k; + for (int c = 1; c <= limit; c++) { + int ok = 1; + for (int u = 0; u < e->n; u++) + if (e->adj[(size_t)v * e->n + u] && e->color[u] == c) { ok = 0; break; } + if (!ok) continue; + e->color[v] = c; + if (color_from(e, v + 1, (c > maxUsed) ? c : maxUsed)) return 1; + e->color[v] = 0; + } + return 0; +} + +Expr* builtin_find_vertex_coloring(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + int n = (int)verts->data.function.arg_count; + if (n == 0) return expr_new_function(expr_new_symbol(SYM_List), NULL, 0); + + char* adj = calloc((size_t)n * (size_t)n, 1); + for (size_t e = 0; e < edges->data.function.arg_count; e++) { + const Expr* ed = edges->data.function.args[e]; + int ia = graph_vertex_index(verts, ed->data.function.args[0]); + int ib = graph_vertex_index(verts, ed->data.function.args[1]); + if (ia < 0 || ib < 0 || ia == ib) continue; + adj[(size_t)ia * n + ib] = adj[(size_t)ib * n + ia] = 1; + } + + int* color = calloc((size_t)n, sizeof(int)); + ColEnv e = { adj, n, 0, color }; + for (int k = 1; k <= n; k++) { + e.k = k; + for (int i = 0; i < n; i++) color[i] = 0; + if (color_from(&e, 0, 0)) break; /* minimal feasible k */ + } + free(adj); + + Expr** items = calloc((size_t)n, sizeof(Expr*)); + for (int i = 0; i < n; i++) items[i] = expr_new_integer(color[i]); + free(color); + Expr* out = expr_new_function(expr_new_symbol(SYM_List), items, (size_t)n); + free(items); + return out; +} diff --git a/src/graph/findvertexcover.c b/src/graph/findvertexcover.c new file mode 100644 index 00000000..67b9e502 --- /dev/null +++ b/src/graph/findvertexcover.c @@ -0,0 +1,92 @@ +/* findvertexcover.c - FindVertexCover[g]: a minimum vertex cover (a smallest set + * of vertices touching every edge) as a vertex list, or {} when g has no edges. + * + * By the Gallai identity a minimum vertex cover is the complement of a maximum + * independent set, so we find a maximum independent set exactly the same way as + * FindIndependentVertexSet -- a maximum clique of the complement graph, via + * branch-and-bound expansion over the non-adjacency -- and return the vertices + * *outside* it. Every edge has at least one endpoint outside any independent + * set, so the complement is a cover, and it is smallest because the independent + * set is largest. Edge direction is ignored; deterministic. Exponential worst + * case, tiny on real graphs. + * + * Unlike FindClique / FindIndependentVertexSet (which return a list containing + * one set), FindVertexCover returns the cover as a flat vertex list, matching + * Wolfram. + * + * Memory (SPEC section 4): returns a fresh List; frees res. NULL on a non-graph. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include +#include + +typedef struct { + const char* nadj; /* complement adjacency: nadj[i*n+j] = i,j non-adjacent */ + int n; + int* best; + int bestlen; +} IndepEnv; + +static void expand(IndepEnv* e, int* R, int rlen, int* P, int plen) { + if (rlen > e->bestlen) { memcpy(e->best, R, (size_t)rlen * sizeof(int)); e->bestlen = rlen; } + for (int i = 0; i < plen; i++) { + if (rlen + (plen - i) <= e->bestlen) break; + int v = P[i]; + R[rlen] = v; + int* newP = (plen - i > 1) ? malloc((size_t)(plen - i - 1) * sizeof(int)) : NULL; + int np = 0; + for (int j = i + 1; j < plen; j++) + if (e->nadj[(size_t)v * e->n + P[j]]) newP[np++] = P[j]; + expand(e, R, rlen + 1, newP, np); + free(newP); + } +} + +Expr* builtin_find_vertex_cover(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + int n = (int)verts->data.function.arg_count; + size_t ne = edges->data.function.arg_count; + + char* nadj = (n > 0) ? malloc((size_t)n * (size_t)n) : NULL; + for (int i = 0; i < n; i++) + for (int j = 0; j < n; j++) + nadj[(size_t)i * n + j] = (i != j); + for (size_t k = 0; k < ne; k++) { + const Expr* ed = edges->data.function.args[k]; + int ia = graph_vertex_index(verts, ed->data.function.args[0]); + int ib = graph_vertex_index(verts, ed->data.function.args[1]); + if (ia < 0 || ib < 0 || ia == ib) continue; + nadj[(size_t)ia * n + ib] = nadj[(size_t)ib * n + ia] = 0; + } + + IndepEnv e = { nadj, n, (n > 0) ? malloc((size_t)n * sizeof(int)) : NULL, 0 }; + if (n > 0) { + int* R = malloc((size_t)n * sizeof(int)); + int* P = malloc((size_t)n * sizeof(int)); + for (int i = 0; i < n; i++) P[i] = i; + expand(&e, R, 0, P, n); + free(R); free(P); + } + free(nadj); + + /* cover = vertices not in the maximum independent set. */ + char* inset = (n > 0) ? calloc((size_t)n, 1) : NULL; + for (int i = 0; i < e.bestlen; i++) inset[e.best[i]] = 1; + int cover = n - e.bestlen; + Expr** vs = (cover > 0) ? calloc((size_t)cover, sizeof(Expr*)) : NULL; + int idx = 0; + for (int v = 0; v < n; v++) if (!inset[v]) vs[idx++] = expr_copy(verts->data.function.args[v]); + free(inset); free(e.best); + + Expr* out = expr_new_function(expr_new_symbol(SYM_List), vs, (size_t)cover); + free(vs); + return out; +} diff --git a/src/graph/friendshipgraph.c b/src/graph/friendshipgraph.c new file mode 100644 index 00000000..cb9e044f --- /dev/null +++ b/src/graph/friendshipgraph.c @@ -0,0 +1,46 @@ +/* friendshipgraph.c - FriendshipGraph[n]: the friendship (windmill) graph F_n, + * n triangles all sharing a single common vertex. + * + * Vertex 1 is the hub; the outer vertices 2..2n+1 are paired (2+2i, 3+2i), and + * each pair forms a triangle with the hub. So there are 2n+1 vertices and 3n + * edges: for each pair, the two spokes hub-a, hub-b and the rim edge a-b. O(n). + * F_1 is the triangle K_3, F_2 the bowtie. By the friendship theorem it is the + * unique graph in which every two vertices have exactly one common neighbour. + * + * Memory (SPEC section 4): returns a freshly-built Graph; frees res. NULL unless + * n >= 1 is an integer. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +Expr* builtin_friendship_graph(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* ne = res->data.function.args[0]; + if (ne->type != EXPR_INTEGER) return NULL; + long n = (long)ne->data.integer; + if (n < 1) return NULL; + + long V = 2 * n + 1; + Expr** vc = malloc((size_t)V * sizeof(Expr*)); + for (long i = 0; i < V; i++) vc[i] = expr_new_integer(i + 1); + + Expr** ec = malloc((size_t)(3 * n) * sizeof(Expr*)); + size_t me = 0; + for (long i = 0; i < n; i++) { + long a = 2 + 2 * i, b = 3 + 2 * i; /* a triangle's rim pair */ + long tri[3][2] = { {1, a}, {1, b}, {a, b} }; + for (int e = 0; e < 3; e++) { + Expr* args[2] = { expr_new_integer(tri[e][0]), expr_new_integer(tri[e][1]) }; + ec[me++] = expr_new_function(expr_new_symbol(SYM_UndirectedEdge), args, 2); + } + } + + Expr* vlist = expr_new_function(expr_new_symbol(SYM_List), vc, (size_t)V); + Expr* elist = expr_new_function(expr_new_symbol(SYM_List), ec, me); + free(vc); free(ec); + Expr* gargs[2] = { vlist, elist }; + return expr_new_function(expr_new_symbol(SYM_Graph), gargs, 2); +} diff --git a/src/graph/geargraph.c b/src/graph/geargraph.c new file mode 100644 index 00000000..00edca4a --- /dev/null +++ b/src/graph/geargraph.c @@ -0,0 +1,45 @@ +/* geargraph.c - GearGraph[n]: the gear (cogwheel) graph, a hub joined to every + * other vertex of a 2n-cycle rim -- equivalently, a wheel with an extra vertex + * inserted between each pair of adjacent rim vertices. + * + * Vertices: hub = 1 and a 2n-cycle rim 2..2n+1. Edges: the rim cycle (2n edges) + * plus n spokes joining the hub to the even-indexed rim vertices. So 2n+1 + * vertices and 3n edges. It is bipartite (the even rim cycle 2-colours, and the + * hub attaches only to one colour class). O(n). + * + * n >= 3 must be an integer. Memory (SPEC section 4): returns a freshly-built + * Graph; frees res. NULL otherwise. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +Expr* builtin_gear_graph(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* ne = res->data.function.args[0]; + if (ne->type != EXPR_INTEGER) return NULL; + long n = (long)ne->data.integer; + if (n < 3) return NULL; + + long rim = 2 * n, V = rim + 1; + Expr** vc = malloc((size_t)V * sizeof(Expr*)); + for (long i = 0; i < V; i++) vc[i] = expr_new_integer(i + 1); + + Expr** ec = malloc((size_t)(3 * n) * sizeof(Expr*)); + size_t me = 0; + #define ADD(a,b) do { Expr* _e[2] = { expr_new_integer(a), expr_new_integer(b) }; \ + ec[me++] = expr_new_function(expr_new_symbol(SYM_UndirectedEdge), _e, 2); } while (0) + for (long r = 0; r < rim; r++) { + ADD(2 + r, 2 + (r + 1) % rim); /* rim cycle */ + if (r % 2 == 0) ADD(1, 2 + r); /* spoke to alternate rim vertices */ + } + #undef ADD + + Expr* vlist = expr_new_function(expr_new_symbol(SYM_List), vc, (size_t)V); + Expr* elist = expr_new_function(expr_new_symbol(SYM_List), ec, me); + free(vc); free(ec); + Expr* gargs[2] = { vlist, elist }; + return expr_new_function(expr_new_symbol(SYM_Graph), gargs, 2); +} diff --git a/src/graph/generalizedpetersen.c b/src/graph/generalizedpetersen.c new file mode 100644 index 00000000..cb031344 --- /dev/null +++ b/src/graph/generalizedpetersen.c @@ -0,0 +1,62 @@ +/* generalizedpetersen.c - GeneralizedPetersenGraph[n, k]: the generalized + * Petersen graph GP(n, k). It has 2n vertices -- an outer n-cycle 1..n and an + * inner set n+1..2n -- with three kinds of edges: the outer cycle o_j ~ o_{j+1}, + * the spokes o_j ~ i_j, and the inner "star polygon" i_j ~ i_{j+k} (indices mod + * n). + * + * A 2n x 2n boolean adjacency dedups the inner edges (when 2k = n they form a + * matching rather than a cycle). O(n^2). Famous members: GP(n,1) is the n-prism + * (GP(4,1) the cube), GP(5,2) is the Petersen graph, GP(6,2) the Durer graph, + * GP(8,3) the Mobius-Kantor graph, GP(10,3) the Desargues graph. + * + * n >= 3 and 1 <= k < n must be integers. + * + * Memory (SPEC section 4): returns a freshly-built Graph; frees res. NULL on bad + * arguments. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +Expr* builtin_generalized_petersen_graph(Expr* res) { + if (res->data.function.arg_count != 2) return NULL; + const Expr* ne = res->data.function.args[0]; + const Expr* ke = res->data.function.args[1]; + if (ne->type != EXPR_INTEGER || ke->type != EXPR_INTEGER) return NULL; + long n = (long)ne->data.integer, k = (long)ke->data.integer; + if (n < 3 || k < 1 || k >= n) return NULL; + + long V = 2 * n; + char* adj = calloc((size_t)V * (size_t)V, 1); + #define SET(a,b) do { adj[(size_t)(a) * V + (b)] = adj[(size_t)(b) * V + (a)] = 1; } while (0) + for (long j = 0; j < n; j++) { + long o = j, oi = n + j; + SET(o, (j + 1) % n); /* outer cycle */ + SET(o, oi); /* spoke */ + long inner = n + (j + k) % n; /* inner star polygon */ + if (inner != oi) SET(oi, inner); + } + #undef SET + + Expr** vc = malloc((size_t)V * sizeof(Expr*)); + for (long i = 0; i < V; i++) vc[i] = expr_new_integer(i + 1); + + size_t cap = (size_t)V * (size_t)(V - 1) / 2; + Expr** ec = malloc(cap * sizeof(Expr*)); + size_t me = 0; + for (long i = 0; i < V; i++) + for (long j = i + 1; j < V; j++) + if (adj[(size_t)i * V + j]) { + Expr* a[2] = { expr_new_integer(i + 1), expr_new_integer(j + 1) }; + ec[me++] = expr_new_function(expr_new_symbol(SYM_UndirectedEdge), a, 2); + } + free(adj); + + Expr* vlist = expr_new_function(expr_new_symbol(SYM_List), vc, (size_t)V); + Expr* elist = expr_new_function(expr_new_symbol(SYM_List), ec, me); + free(vc); free(ec); + Expr* gargs[2] = { vlist, elist }; + return expr_new_function(expr_new_symbol(SYM_Graph), gargs, 2); +} diff --git a/src/graph/generators.c b/src/graph/generators.c new file mode 100644 index 00000000..3090e1a3 --- /dev/null +++ b/src/graph/generators.c @@ -0,0 +1,242 @@ +/* generators.c - standard graph constructors. + * + * CompleteGraph[n] - undirected K_n (all n(n-1)/2 edges) + * CycleGraph[n] - undirected cycle on 1..n + * PathGraph[n] - undirected path 1-2-...-n + * PathGraph[{v1,...,vk}] - undirected path over the given vertices + * RandomGraph[{n, m}] - undirected graph with n vertices, m random edges + * + * Each assembles a Graph[List verts, List edges] expression and returns it; the + * evaluator canonicalizes and validates it via builtin_graph. Vertices are the + * integers 1..n (except the explicit PathGraph[{...}] form). RandomGraph reuses + * the system RNG by evaluating RandomSample over the candidate edges, so it + * honors SeedRandom. + * + * Memory (SPEC section 4): returns freshly-allocated trees; frees res. + */ + +#include "graph.h" +#include "expr.h" +#include "eval.h" +#include "sym_names.h" +#include + +/* Small integer argument as a nonnegative long, or -1 if not a suitable int. */ +static long as_count(const Expr* e) { + if (!e || e->type != EXPR_INTEGER || e->data.integer < 0) return -1; + return (long)e->data.integer; +} + +static Expr* undirected_edge(long a, long b) { + Expr* ea[2] = { expr_new_integer(a), expr_new_integer(b) }; + return expr_new_function(expr_new_symbol(SYM_UndirectedEdge), ea, 2); +} + +/* Wrap vertex/edge C-arrays into a Graph[...] (moves ownership). */ +static Expr* make_graph(Expr** verts, size_t nv, Expr** edges, size_t ne) { + Expr* vlist = expr_new_function(expr_new_symbol(SYM_List), verts, nv); + Expr* elist = expr_new_function(expr_new_symbol(SYM_List), edges, ne); + Expr* gargs[2] = { vlist, elist }; + return expr_new_function(expr_new_symbol(SYM_Graph), gargs, 2); +} + +static Expr** int_vertices(long n) { + Expr** v = (n > 0) ? calloc((size_t)n, sizeof(Expr*)) : NULL; + for (long i = 0; i < n; i++) v[i] = expr_new_integer(i + 1); + return v; +} + +/* CompleteGraph[{n1,n2,...}] - the complete multipartite graph: vertices split + * into parts of the given sizes, with an edge between every pair in DIFFERENT + * parts and none within a part. CompleteGraph[{m,n}] is complete bipartite. */ +static Expr* complete_multipartite(const Expr* spec) { + size_t parts = spec->data.function.arg_count; + long total = 0; + for (size_t p = 0; p < parts; p++) { + long s = as_count(spec->data.function.args[p]); + if (s < 0) return NULL; + total += s; + } + if (total <= 0) return make_graph(int_vertices(total > 0 ? total : 0), + (size_t)(total > 0 ? total : 0), NULL, 0); + /* group[v] = index of the part vertex v (0-based) belongs to. */ + int* group = malloc((size_t)total * sizeof(int)); + long v = 0; + for (size_t p = 0; p < parts; p++) { + long s = as_count(spec->data.function.args[p]); + for (long i = 0; i < s; i++) group[v++] = (int)p; + } + size_t cap = (size_t)total * (size_t)(total - 1) / 2; + Expr** edges = (cap > 0) ? calloc(cap, sizeof(Expr*)) : NULL; + size_t k = 0; + for (long i = 0; i < total; i++) + for (long j = i + 1; j < total; j++) + if (group[i] != group[j]) edges[k++] = undirected_edge(i + 1, j + 1); + free(group); + return make_graph(int_vertices(total), (size_t)total, edges, k); +} + +Expr* builtin_complete_graph(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* arg = res->data.function.args[0]; + if (graph_is_list(arg)) return complete_multipartite(arg); /* K_{n1,n2,...} */ + long n = as_count(arg); + if (n < 0) return NULL; + size_t ne = (size_t)n * (size_t)(n - 1) / 2; + Expr** edges = (ne > 0) ? calloc(ne, sizeof(Expr*)) : NULL; + size_t k = 0; + for (long i = 1; i <= n; i++) + for (long j = i + 1; j <= n; j++) + edges[k++] = undirected_edge(i, j); + return make_graph(int_vertices(n), (size_t)n, edges, ne); +} + +Expr* builtin_cycle_graph(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + long n = as_count(res->data.function.args[0]); + if (n < 0) return NULL; + /* Path edges 1-2-...-n, plus the wrap edge n-1 when n >= 3 (for n <= 2 the + * wrap edge would duplicate an existing one). */ + size_t ne = (n >= 3) ? (size_t)n : (n >= 2 ? 1u : 0u); + Expr** edges = (ne > 0) ? calloc(ne, sizeof(Expr*)) : NULL; + size_t k = 0; + for (long i = 1; i < n; i++) edges[k++] = undirected_edge(i, i + 1); + if (n >= 3) edges[k++] = undirected_edge(n, 1); + return make_graph(int_vertices(n), (size_t)n, edges, ne); +} + +Expr* builtin_path_graph(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* arg = res->data.function.args[0]; + + if (graph_is_list(arg)) { + /* PathGraph[{v1,...,vk}] over explicit vertices. */ + size_t nv = arg->data.function.arg_count; + Expr** verts = (nv > 0) ? calloc(nv, sizeof(Expr*)) : NULL; + for (size_t i = 0; i < nv; i++) verts[i] = expr_copy(arg->data.function.args[i]); + size_t ne = (nv > 0) ? nv - 1 : 0; + Expr** edges = (ne > 0) ? calloc(ne, sizeof(Expr*)) : NULL; + for (size_t i = 0; i + 1 < nv; i++) { + Expr* ea[2] = { expr_copy(arg->data.function.args[i]), + expr_copy(arg->data.function.args[i + 1]) }; + edges[i] = expr_new_function(expr_new_symbol(SYM_UndirectedEdge), ea, 2); + } + return make_graph(verts, nv, edges, ne); + } + + long n = as_count(arg); + if (n < 0) return NULL; + size_t ne = (n > 0) ? (size_t)n - 1 : 0; + Expr** edges = (ne > 0) ? calloc(ne, sizeof(Expr*)) : NULL; + for (long i = 1; i < n; i++) edges[i - 1] = undirected_edge(i, i + 1); + return make_graph(int_vertices(n), (size_t)n, edges, ne); +} + +Expr* builtin_random_graph(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* spec = res->data.function.args[0]; + if (!graph_is_list(spec) || spec->data.function.arg_count != 2) return NULL; + long n = as_count(spec->data.function.args[0]); + long m = as_count(spec->data.function.args[1]); + if (n < 0 || m < 0) return NULL; + long maxe = n * (n - 1) / 2; + if (m > maxe) return NULL; /* more edges than a simple graph allows */ + + /* All candidate undirected edges. */ + size_t ncand = (size_t)maxe; + Expr** cand = (ncand > 0) ? calloc(ncand, sizeof(Expr*)) : NULL; + size_t k = 0; + for (long i = 1; i <= n; i++) + for (long j = i + 1; j <= n; j++) + cand[k++] = undirected_edge(i, j); + Expr* cand_list = expr_new_function(expr_new_symbol(SYM_List), cand, ncand); + free(cand); + + /* Sample m of them without replacement, via the seeded system RNG. */ + Expr* sample_args[2] = { cand_list, expr_new_integer(m) }; + Expr* sample_call = expr_new_function(expr_new_symbol("RandomSample"), + sample_args, 2); + Expr* sampled = evaluate(sample_call); /* consumes sample_call */ + if (!graph_is_list(sampled)) { expr_free(sampled); return NULL; } + + Expr* gargs[2] = { expr_new_function(expr_new_symbol(SYM_List), + int_vertices(n), (size_t)n), + sampled }; + return expr_new_function(expr_new_symbol(SYM_Graph), gargs, 2); +} + +/* StarGraph[n] - a central vertex 1 joined to the n-1 leaves 2..n (K_{1,n-1}). */ +Expr* builtin_star_graph(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + long n = as_count(res->data.function.args[0]); + if (n < 1) return NULL; + size_t ne = (size_t)(n - 1); + Expr** edges = (ne > 0) ? calloc(ne, sizeof(Expr*)) : NULL; + for (long i = 2; i <= n; i++) edges[i - 2] = undirected_edge(1, i); + return make_graph(int_vertices(n), (size_t)n, edges, ne); +} + +/* WheelGraph[n] - a rim cycle on vertices 1..n-1 plus a hub n joined to every + * rim vertex (2(n-1) edges). Needs n >= 4 for a rim cycle of length >= 3 + * (W_4 = K_4); smaller n is left unevaluated. */ +Expr* builtin_wheel_graph(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + long n = as_count(res->data.function.args[0]); + if (n < 4) return NULL; + long rim = n - 1; /* rim vertices 1..rim; hub = n */ + size_t ne = (size_t)(2 * rim); + Expr** edges = calloc(ne, sizeof(Expr*)); + size_t k = 0; + for (long i = 1; i <= rim; i++) edges[k++] = undirected_edge(i, (i < rim) ? i + 1 : 1); + for (long i = 1; i <= rim; i++) edges[k++] = undirected_edge(n, i); + return make_graph(int_vertices(n), (size_t)n, edges, ne); +} + +/* GridGraph[{d1,d2,...}] - the k-dimensional grid: vertices are the cells of a + * d1 x d2 x ... lattice (row-major, 1..prod(di)); two cells are adjacent when + * they differ by 1 in exactly one coordinate. GridGraph[{n}] is a path. */ +Expr* builtin_grid_graph(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* spec = res->data.function.args[0]; + if (!graph_is_list(spec)) return NULL; + size_t k = spec->data.function.arg_count; + if (k == 0) return NULL; + long* dim = malloc(k * sizeof(long)); + long* stride = malloc(k * sizeof(long)); + long total = 1; + for (size_t i = 0; i < k; i++) { + long d = as_count(spec->data.function.args[i]); + if (d < 1 || total > 200000 / (d > 0 ? d : 1)) { free(dim); free(stride); return NULL; } + dim[i] = d; total *= d; + } + stride[k - 1] = 1; + for (long i = (long)k - 2; i >= 0; i--) stride[i] = stride[i + 1] * dim[i + 1]; + + Expr** edges = (total > 0) ? calloc((size_t)total * k, sizeof(Expr*)) : NULL; + size_t m = 0; + for (long v = 0; v < total; v++) + for (size_t i = 0; i < k; i++) { + long coord = (v / stride[i]) % dim[i]; + if (coord + 1 < dim[i]) edges[m++] = undirected_edge(v + 1, v + stride[i] + 1); + } + free(dim); free(stride); + return make_graph(int_vertices(total), (size_t)total, edges, m); +} + +/* HypercubeGraph[k] - the k-cube Q_k: 2^k vertices (the k-bit strings), adjacent + * when they differ in exactly one bit. k-regular, bipartite; Q_2 = C_4. */ +Expr* builtin_hypercube_graph(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + long k = as_count(res->data.function.args[0]); + if (k < 0 || k > 16) return NULL; /* guard 2^k blow-up */ + long total = 1L << k; + size_t cap = (size_t)k * (size_t)(total / 2 > 0 ? total / 2 : 0); + Expr** edges = (cap > 0) ? calloc(cap, sizeof(Expr*)) : NULL; + size_t m = 0; + for (long v = 0; v < total; v++) + for (long b = 0; b < k; b++) { + long nb = v ^ (1L << b); + if (v < nb) edges[m++] = undirected_edge(v + 1, nb + 1); + } + return make_graph(int_vertices(total), (size_t)total, edges, m); +} diff --git a/src/graph/globalclustering.c b/src/graph/globalclustering.c new file mode 100644 index 00000000..fc2f335f --- /dev/null +++ b/src/graph/globalclustering.c @@ -0,0 +1,67 @@ +/* globalclustering.c - GlobalClusteringCoefficient[g]: the graph's transitivity, + * the fraction of connected vertex triples that are closed into triangles: + * + * C = 3 * (#triangles) / (#connected triples) = (sum_v L_v) / (sum_v C(d_v,2)) + * + * where L_v is the number of edges among v's neighbours (so sum_v L_v counts + * each triangle three times = 3T) and C(d_v,2) is the number of paths of length + * two centred at v. Rewritten to stay integral, C = 2*sum_v L_v / sum_v + * d_v(d_v-1). When there are no connected triples (no vertex of degree >= 2) the + * coefficient is 0 by convention. + * + * Distinct from the mean of the local coefficients: transitivity weights each + * vertex by how many triples it anchors. Edge direction is ignored (underlying + * undirected graph). Symmetric boolean adjacency, O(V * d_max^2). Exact rational. + * + * Memory (SPEC section 4): returns a fresh number; frees res. NULL on a + * non-graph argument. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include "eval.h" +#include + +/* Exact num/den as a reduced value (integer or Rational). den > 0 required. */ +static Expr* ratio(long num, long den) { + Expr* pa[2] = { expr_new_integer(den), expr_new_integer(-1) }; + Expr* inv = expr_new_function(expr_new_symbol(SYM_Power), pa, 2); + Expr* ta[2] = { expr_new_integer(num), inv }; + return evaluate(expr_new_function(expr_new_symbol(SYM_Times), ta, 2)); +} + +Expr* builtin_global_clustering_coefficient(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + int n = (int)verts->data.function.arg_count; + size_t ne = edges->data.function.arg_count; + + char* adj = (n > 0) ? calloc((size_t)n * (size_t)n, 1) : NULL; + for (size_t e = 0; e < ne; e++) { + const Expr* ed = edges->data.function.args[e]; + int ia = graph_vertex_index(verts, ed->data.function.args[0]); + int ib = graph_vertex_index(verts, ed->data.function.args[1]); + if (ia < 0 || ib < 0 || ia == ib) continue; + adj[(size_t)ia * n + ib] = adj[(size_t)ib * n + ia] = 1; + } + + long sumL = 0, sumDD = 0; /* sum_v L_v ; sum_v d_v(d_v-1) */ + int* nbr = (n > 0) ? malloc((size_t)n * sizeof(int)) : NULL; + for (int v = 0; v < n; v++) { + int d = 0; + for (int u = 0; u < n; u++) if (adj[(size_t)v * n + u]) nbr[d++] = u; + sumDD += (long)d * (d - 1); + for (int i = 0; i < d; i++) + for (int j = i + 1; j < d; j++) + if (adj[(size_t)nbr[i] * n + nbr[j]]) sumL++; + } + free(nbr); free(adj); + + if (sumDD == 0) return expr_new_integer(0); /* no connected triples */ + return ratio(2 * sumL, sumDD); +} diff --git a/src/graph/graph.c b/src/graph/graph.c new file mode 100644 index 00000000..b5c58b02 --- /dev/null +++ b/src/graph/graph.c @@ -0,0 +1,753 @@ +/* graph_init - the graph-subsystem module entry point. + * + * Registers the per-builtin symbols, attributes, and docstrings. Each builtin + * lives in its own translation unit inside src/graph/ (construct.c, graphq.c, + * ...), mirroring the src/linalg/ layout. graph_init() is called from + * core_init() in src/core.c. + * + * Phase 1 registers Graph (construction/normalization, in construct.c) and + * GraphQ (in graphq.c). Queries, matrix views, generators, algorithms, and + * visualization arrive in later phases, each adding its registrations here. + * The builtin implementations themselves live one-per-file in src/graph/. + */ + +#include "graph.h" +#include "symtab.h" +#include "attr.h" + +void graph_init(void) { + /* Graph -- construction, normalization, canonicalization (construct.c). */ + symtab_add_builtin("Graph", builtin_graph); + symtab_get_def("Graph")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("Graph", + "Graph[v, e] represents a graph with vertices v and edges e. " + "Graph[e] derives the vertices from the edge list. Edges are " + "DirectedEdge[u,v] or UndirectedEdge[u,v]; u->v and u<->v are accepted " + "as shorthand. Simple graphs only: no self-loops or parallel edges."); + + /* GraphQ -- validity predicate (graphq.c). */ + symtab_add_builtin("GraphQ", builtin_graph_q); + symtab_get_def("GraphQ")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("GraphQ", + "GraphQ[g] gives True if g is a valid graph, and False otherwise."); + + /* ---- Phase 2: query / representation builtins ------------------------- */ + symtab_add_builtin("VertexList", builtin_vertex_list); + symtab_get_def("VertexList")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("VertexList", + "VertexList[g] gives the list of vertices of the graph g."); + + symtab_add_builtin("EdgeList", builtin_edge_list); + symtab_get_def("EdgeList")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("EdgeList", + "EdgeList[g] gives the list of edges of the graph g."); + + symtab_add_builtin("VertexCount", builtin_vertex_count); + symtab_get_def("VertexCount")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("VertexCount", + "VertexCount[g] gives the number of vertices in the graph g."); + + symtab_add_builtin("EdgeCount", builtin_edge_count); + symtab_get_def("EdgeCount")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("EdgeCount", + "EdgeCount[g] gives the number of edges in the graph g."); + + symtab_add_builtin("AdjacencyList", builtin_adjacency_list); + symtab_get_def("AdjacencyList")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("AdjacencyList", + "AdjacencyList[g] gives the adjacency list of g; AdjacencyList[g,v] " + "gives the vertices adjacent to v (successors for directed edges)."); + + symtab_add_builtin("VertexDegree", builtin_vertex_degree); + symtab_get_def("VertexDegree")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("VertexDegree", + "VertexDegree[g] gives the list of vertex degrees; VertexDegree[g,v] " + "gives the degree of vertex v."); + + symtab_add_builtin("VertexInDegree", builtin_vertex_in_degree); + symtab_get_def("VertexInDegree")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("VertexInDegree", + "VertexInDegree[g] / VertexInDegree[g,v] gives in-degrees " + "(incoming directed edges; undirected edges count for both)."); + + symtab_add_builtin("VertexOutDegree", builtin_vertex_out_degree); + symtab_get_def("VertexOutDegree")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("VertexOutDegree", + "VertexOutDegree[g] / VertexOutDegree[g,v] gives out-degrees " + "(outgoing directed edges; undirected edges count for both)."); + + symtab_add_builtin("DirectedGraphQ", builtin_directed_graph_q); + symtab_get_def("DirectedGraphQ")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("DirectedGraphQ", + "DirectedGraphQ[g] gives True if all edges of g are directed."); + + /* ---- Phase 3: matrix views (linalg interop) -------------------------- */ + symtab_add_builtin("AdjacencyMatrix", builtin_adjacency_matrix); + symtab_get_def("AdjacencyMatrix")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("AdjacencyMatrix", + "AdjacencyMatrix[g] gives the 0/1 adjacency matrix of g (symmetric for " + "undirected graphs)."); + + symtab_add_builtin("IncidenceMatrix", builtin_incidence_matrix); + symtab_get_def("IncidenceMatrix")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("IncidenceMatrix", + "IncidenceMatrix[g] gives the vertex-edge incidence matrix of g " + "(oriented: -1 tail, +1 head for directed edges)."); + + symtab_add_builtin("AdjacencyGraph", builtin_adjacency_graph); + symtab_get_def("AdjacencyGraph")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("AdjacencyGraph", + "AdjacencyGraph[m] builds a graph on vertices 1..n from a 0/1 adjacency " + "matrix m (undirected if m is symmetric, else directed)."); + + /* ---- Phase 4: graph generators --------------------------------------- */ + symtab_add_builtin("CompleteGraph", builtin_complete_graph); + symtab_get_def("CompleteGraph")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("CompleteGraph", + "CompleteGraph[n] gives the complete graph K_n on n vertices."); + + symtab_add_builtin("CycleGraph", builtin_cycle_graph); + symtab_get_def("CycleGraph")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("CycleGraph", + "CycleGraph[n] gives the cycle graph on n vertices."); + + symtab_add_builtin("PathGraph", builtin_path_graph); + symtab_get_def("PathGraph")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("PathGraph", + "PathGraph[n] gives the path on n vertices; PathGraph[{v1,...}] the " + "path over the given vertices."); + + symtab_add_builtin("RandomGraph", builtin_random_graph); + symtab_get_def("RandomGraph")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("RandomGraph", + "RandomGraph[{n, m}] gives a random undirected graph with n vertices " + "and m edges."); + + symtab_add_builtin("StarGraph", builtin_star_graph); + symtab_get_def("StarGraph")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("StarGraph", + "StarGraph[n] gives the star K_{1,n-1}: a central vertex joined to n-1 " + "leaves."); + + symtab_add_builtin("WheelGraph", builtin_wheel_graph); + symtab_get_def("WheelGraph")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("WheelGraph", + "WheelGraph[n] gives the wheel on n vertices: a cycle of n-1 rim " + "vertices plus a hub joined to all of them (n >= 4)."); + + symtab_add_builtin("GridGraph", builtin_grid_graph); + symtab_get_def("GridGraph")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("GridGraph", + "GridGraph[{d1,d2,...}] gives the k-dimensional grid graph on a " + "d1 x d2 x ... lattice (cells adjacent when they differ by 1 in one " + "coordinate)."); + + symtab_add_builtin("HypercubeGraph", builtin_hypercube_graph); + symtab_get_def("HypercubeGraph")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("HypercubeGraph", + "HypercubeGraph[k] gives the k-dimensional hypercube Q_k: 2^k vertices " + "adjacent when they differ in one bit (k-regular, bipartite)."); + + /* ---- Phase 5: search & computation algorithms ------------------------ */ + symtab_add_builtin("FindShortestPath", builtin_find_shortest_path); + symtab_get_def("FindShortestPath")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("FindShortestPath", + "FindShortestPath[g,s,t] gives a shortest path from s to t as a list of " + "vertices ({} if none)."); + + symtab_add_builtin("GraphDistance", builtin_graph_distance); + symtab_get_def("GraphDistance")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("GraphDistance", + "GraphDistance[g,s,t] gives the length of a shortest path from s to t " + "(Infinity if unreachable)."); + + symtab_add_builtin("ConnectedComponents", builtin_connected_components); + symtab_get_def("ConnectedComponents")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("ConnectedComponents", + "ConnectedComponents[g] gives the connected components of g (weak, on " + "the underlying undirected graph)."); + + symtab_add_builtin("WeaklyConnectedComponents", builtin_weakly_connected_components); + symtab_get_def("WeaklyConnectedComponents")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("WeaklyConnectedComponents", + "WeaklyConnectedComponents[g] gives the weakly connected components of g."); + + symtab_add_builtin("StronglyConnectedComponents", builtin_strongly_connected_components); + symtab_get_def("StronglyConnectedComponents")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("StronglyConnectedComponents", + "StronglyConnectedComponents[g] gives the strongly connected components " + "of g (following edge directions)."); + + symtab_add_builtin("FindSpanningTree", builtin_find_spanning_tree); + symtab_get_def("FindSpanningTree")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("FindSpanningTree", + "FindSpanningTree[g] gives a spanning tree (forest) of g as a graph."); + + symtab_add_builtin("ConnectedGraphQ", builtin_connected_graph_q); + symtab_get_def("ConnectedGraphQ")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("ConnectedGraphQ", + "ConnectedGraphQ[g] gives True if g is connected."); + + symtab_add_builtin("VertexConnectivity", builtin_vertex_connectivity); + symtab_get_def("VertexConnectivity")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("VertexConnectivity", + "VertexConnectivity[g] gives the minimum number of vertices whose " + "removal disconnects g."); + + symtab_add_builtin("BipartiteGraphQ", builtin_bipartite_graph_q); + symtab_get_def("BipartiteGraphQ")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("BipartiteGraphQ", + "BipartiteGraphQ[g] gives True if the underlying undirected graph is " + "2-colorable (has no odd cycle), and False otherwise."); + + symtab_add_builtin("VertexEccentricity", builtin_vertex_eccentricity); + symtab_get_def("VertexEccentricity")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("VertexEccentricity", + "VertexEccentricity[g,v] gives the greatest shortest-path distance from " + "v to any vertex; VertexEccentricity[g] gives the list for all vertices " + "(Infinity if some vertex is unreachable)."); + + symtab_add_builtin("GraphDiameter", builtin_graph_diameter); + symtab_get_def("GraphDiameter")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("GraphDiameter", + "GraphDiameter[g] gives the maximum vertex eccentricity (Infinity if g " + "is not strongly connected)."); + + symtab_add_builtin("GraphRadius", builtin_graph_radius); + symtab_get_def("GraphRadius")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("GraphRadius", + "GraphRadius[g] gives the minimum vertex eccentricity (Infinity if no " + "vertex reaches all others)."); + + symtab_add_builtin("GraphCenter", builtin_graph_center); + symtab_get_def("GraphCenter")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("GraphCenter", + "GraphCenter[g] gives the vertices whose eccentricity equals the graph " + "radius."); + + symtab_add_builtin("GraphPeriphery", builtin_graph_periphery); + symtab_get_def("GraphPeriphery")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("GraphPeriphery", + "GraphPeriphery[g] gives the vertices whose eccentricity equals the graph " + "diameter."); + + symtab_add_builtin("AcyclicGraphQ", builtin_acyclic_graph_q); + symtab_get_def("AcyclicGraphQ")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("AcyclicGraphQ", + "AcyclicGraphQ[g] gives True if g has no cycle: a DAG for a directed " + "graph, a forest for an undirected one."); + + symtab_add_builtin("TopologicalSort", builtin_topological_sort); + symtab_get_def("TopologicalSort")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("TopologicalSort", + "TopologicalSort[g] gives a vertex ordering in which every edge points " + "forward, or $Failed if g is not a directed acyclic graph."); + + symtab_add_builtin("GraphComplement", builtin_graph_complement); + symtab_get_def("GraphComplement")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("GraphComplement", + "GraphComplement[g] gives the graph on the same vertices whose edges are " + "exactly the non-edges of g (directed graphs stay directed)."); + + symtab_add_builtin("KirchhoffMatrix", builtin_kirchhoff_matrix); + symtab_get_def("KirchhoffMatrix")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("KirchhoffMatrix", + "KirchhoffMatrix[g] gives the graph Laplacian D - A (degree diagonal " + "minus adjacency matrix); each row sums to 0."); + + symtab_add_builtin("EdgeConnectivity", builtin_edge_connectivity); + symtab_get_def("EdgeConnectivity")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("EdgeConnectivity", + "EdgeConnectivity[g] gives the minimum number of edges whose removal " + "disconnects g (0 if g is already disconnected)."); + + symtab_add_builtin("LineGraph", builtin_line_graph); + symtab_get_def("LineGraph")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("LineGraph", + "LineGraph[g] gives the line graph of g: its vertices are the edges of " + "g, adjacent when they share an endpoint (head-to-tail if directed)."); + + symtab_add_builtin("EulerianGraphQ", builtin_eulerian_graph_q); + symtab_get_def("EulerianGraphQ")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("EulerianGraphQ", + "EulerianGraphQ[g] gives True if g has an Eulerian cycle: connected with " + "all even degrees (undirected), or in-degree = out-degree everywhere " + "(directed)."); + + symtab_add_builtin("ClosenessCentrality", builtin_closeness_centrality); + symtab_get_def("ClosenessCentrality")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("ClosenessCentrality", + "ClosenessCentrality[g] gives the list of closeness centralities " + "c_i = (r_i-1)^2/((n-1) S_i), where r_i vertices are reachable from i at " + "total distance S_i; larger means more central."); + + symtab_add_builtin("TransitiveClosure", builtin_transitive_closure); + symtab_get_def("TransitiveClosure")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("TransitiveClosure", + "TransitiveClosure[g] adds an edge u->v whenever v is reachable from u " + "(directed); for an undirected graph each connected component becomes a " + "complete graph."); + + symtab_add_builtin("BetweennessCentrality", builtin_betweenness_centrality); + symtab_get_def("BetweennessCentrality")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("BetweennessCentrality", + "BetweennessCentrality[g] gives, for each vertex, the number of shortest " + "paths passing through it (fractional when paths tie); undirected pairs " + "counted once."); + + symtab_add_builtin("FindEulerianCycle", builtin_find_eulerian_cycle); + symtab_get_def("FindEulerianCycle")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("FindEulerianCycle", + "FindEulerianCycle[g] gives an Eulerian cycle (a closed walk using every " + "edge once) as a vertex list, or {} if g has none."); + + symtab_add_builtin("FindHamiltonianCycle", builtin_find_hamiltonian_cycle); + symtab_get_def("FindHamiltonianCycle")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("FindHamiltonianCycle", + "FindHamiltonianCycle[g] gives a Hamiltonian cycle (a closed walk " + "visiting every vertex once) as a vertex list, or {} if g has none."); + + symtab_add_builtin("GraphPower", builtin_graph_power); + symtab_get_def("GraphPower")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("GraphPower", + "GraphPower[g, k] gives the k-th power of g: the graph on the same " + "vertices joining two vertices whenever g has a path of length at most k " + "between them."); + + symtab_add_builtin("FindCycle", builtin_find_cycle); + symtab_get_def("FindCycle")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("FindCycle", + "FindCycle[g] gives a cycle in g as a list containing one list of its " + "edges, or {} if g is acyclic."); + + symtab_add_builtin("GraphDistanceMatrix", builtin_graph_distance_matrix); + symtab_get_def("GraphDistanceMatrix")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("GraphDistanceMatrix", + "GraphDistanceMatrix[g] gives the matrix whose (i,j) entry is the " + "shortest-path distance from vertex i to vertex j (Infinity if " + "unreachable)."); + + symtab_add_builtin("GraphDensity", builtin_graph_density); + symtab_get_def("GraphDensity")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("GraphDensity", + "GraphDensity[g] gives the fraction of possible edges present in g, an " + "exact rational in [0, 1] (1 for a complete graph, 0 for an empty one)."); + + symtab_add_builtin("DegreeCentrality", builtin_degree_centrality); + symtab_get_def("DegreeCentrality")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("DegreeCentrality", + "DegreeCentrality[g] gives, for each vertex, the number of incident " + "edges (degree; in-degree + out-degree for a directed graph)."); + + symtab_add_builtin("FindHamiltonianPath", builtin_find_hamiltonian_path); + symtab_get_def("FindHamiltonianPath")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("FindHamiltonianPath", + "FindHamiltonianPath[g] gives a Hamiltonian path (a walk visiting every " + "vertex once) as a vertex list, or {} if g has none."); + + symtab_add_builtin("KCoreComponents", builtin_kcore_components); + symtab_get_def("KCoreComponents")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("KCoreComponents", + "KCoreComponents[g, k] gives the connected components of the k-core of g " + "(the maximal subgraph in which every vertex has degree at least k), as " + "a list of vertex lists."); + + symtab_add_builtin("LocalClusteringCoefficient", builtin_local_clustering_coefficient); + symtab_get_def("LocalClusteringCoefficient")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("LocalClusteringCoefficient", + "LocalClusteringCoefficient[g] gives, for each vertex, the fraction of " + "its neighbor pairs that are adjacent (0 for degree < 2)."); + + symtab_add_builtin("GlobalClusteringCoefficient", builtin_global_clustering_coefficient); + symtab_get_def("GlobalClusteringCoefficient")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("GlobalClusteringCoefficient", + "GlobalClusteringCoefficient[g] gives the graph transitivity: three " + "times the number of triangles divided by the number of connected vertex " + "triples (0 when there are none)."); + + symtab_add_builtin("MeanClusteringCoefficient", builtin_mean_clustering_coefficient); + symtab_get_def("MeanClusteringCoefficient")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("MeanClusteringCoefficient", + "MeanClusteringCoefficient[g] gives the average of the local clustering " + "coefficients over all vertices."); + + symtab_add_builtin("FindClique", builtin_find_clique); + symtab_get_def("FindClique")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("FindClique", + "FindClique[g] gives a largest clique (a set of pairwise-adjacent " + "vertices) as a list containing one vertex list."); + + symtab_add_builtin("FindIndependentVertexSet", builtin_find_independent_vertex_set); + symtab_get_def("FindIndependentVertexSet")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("FindIndependentVertexSet", + "FindIndependentVertexSet[g] gives a largest independent vertex set (a " + "set of pairwise non-adjacent vertices) as a list containing one vertex " + "list."); + + symtab_add_builtin("FindVertexCover", builtin_find_vertex_cover); + symtab_get_def("FindVertexCover")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("FindVertexCover", + "FindVertexCover[g] gives a minimum vertex cover (a smallest set of " + "vertices touching every edge) as a vertex list."); + + symtab_add_builtin("GraphReciprocity", builtin_graph_reciprocity); + symtab_get_def("GraphReciprocity")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("GraphReciprocity", + "GraphReciprocity[g] gives the fraction of arcs whose reverse is also " + "present (1 for an undirected graph), as an exact rational."); + + symtab_add_builtin("ChromaticPolynomial", builtin_chromatic_polynomial); + symtab_get_def("ChromaticPolynomial")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("ChromaticPolynomial", + "ChromaticPolynomial[g, k] gives the chromatic polynomial of g in k: the " + "number of proper k-colorings (a polynomial for symbolic k)."); + + symtab_add_builtin("ChromaticNumber", builtin_chromatic_number); + symtab_get_def("ChromaticNumber")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("ChromaticNumber", + "ChromaticNumber[g] gives the least number of colors needed to color g " + "so that adjacent vertices differ."); + + symtab_add_builtin("DegreeSequence", builtin_degree_sequence); + symtab_get_def("DegreeSequence")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("DegreeSequence", + "DegreeSequence[g] gives the vertex degrees (in-degree + out-degree for " + "a directed graph) sorted in non-increasing order."); + + symtab_add_builtin("TreeGraphQ", builtin_tree_graph_q); + symtab_get_def("TreeGraphQ")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("TreeGraphQ", + "TreeGraphQ[g] gives True iff g is a tree: connected with no cycles " + "(n-1 edges on n>=1 vertices)."); + + symtab_add_builtin("StronglyConnectedGraphQ", builtin_strongly_connected_graph_q); + symtab_get_def("StronglyConnectedGraphQ")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("StronglyConnectedGraphQ", + "StronglyConnectedGraphQ[g] gives True iff every vertex is reachable " + "from every other following edge directions."); + + symtab_add_builtin("HamiltonianGraphQ", builtin_hamiltonian_graph_q); + symtab_get_def("HamiltonianGraphQ")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("HamiltonianGraphQ", + "HamiltonianGraphQ[g] gives True iff g has a Hamiltonian cycle (a closed " + "walk visiting every vertex once)."); + + symtab_add_builtin("RegularGraphQ", builtin_regular_graph_q); + symtab_get_def("RegularGraphQ")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("RegularGraphQ", + "RegularGraphQ[g] gives True iff every vertex has the same degree " + "(equal in- and out-degrees for a directed graph)."); + + symtab_add_builtin("CompleteGraphQ", builtin_complete_graph_q); + symtab_get_def("CompleteGraphQ")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("CompleteGraphQ", + "CompleteGraphQ[g] gives True iff every pair of distinct vertices in g " + "is adjacent."); + + symtab_add_builtin("GraphUnion", builtin_graph_union); + symtab_get_def("GraphUnion")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("GraphUnion", + "GraphUnion[g1, g2] gives the graph with the union of the vertices and " + "the union of the edges of g1 and g2."); + + symtab_add_builtin("GraphIntersection", builtin_graph_intersection); + symtab_get_def("GraphIntersection")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("GraphIntersection", + "GraphIntersection[g1, g2] gives the graph with the vertices common to " + "g1 and g2 and the edges present in both."); + + symtab_add_builtin("GraphDifference", builtin_graph_difference); + symtab_get_def("GraphDifference")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("GraphDifference", + "GraphDifference[g1, g2] gives the graph on g1's vertices with the edges " + "of g1 that are not in g2."); + + symtab_add_builtin("ReverseGraph", builtin_graph_reverse); + symtab_get_def("ReverseGraph")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("ReverseGraph", + "ReverseGraph[g] reverses the direction of every edge of g (undirected " + "edges are unchanged)."); + + symtab_add_builtin("PathGraphQ", builtin_path_graph_q); + symtab_get_def("PathGraphQ")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("PathGraphQ", + "PathGraphQ[g] gives True iff g is a path graph (a tree with maximum " + "degree at most 2)."); + + symtab_add_builtin("VertexContract", builtin_vertex_contract); + symtab_get_def("VertexContract")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("VertexContract", + "VertexContract[g, {v1, v2, ...}] merges the listed vertices into one, " + "redirecting edges, dropping self-loops, and collapsing parallel edges."); + + symtab_add_builtin("PageRankCentrality", builtin_pagerank_centrality); + symtab_get_def("PageRankCentrality")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("PageRankCentrality", + "PageRankCentrality[g] gives the PageRank of each vertex (damping 17/20) " + "as an exact rational probability vector summing to 1."); + + symtab_add_builtin("KatzCentrality", builtin_katz_centrality); + symtab_get_def("KatzCentrality")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("KatzCentrality", + "KatzCentrality[g, alpha] gives the Katz centrality of each vertex with " + "attenuation alpha (base weight 1), solving (I - alpha A^T) x = 1 exactly."); + + symtab_add_builtin("GraphJoin", builtin_graph_join); + symtab_get_def("GraphJoin")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("GraphJoin", + "GraphJoin[g1, g2] gives the join of g1 and g2: their disjoint union " + "(vertices relabeled 1..n1+n2) plus every edge between the two blocks."); + + symtab_add_builtin("IndexGraph", builtin_index_graph); + symtab_get_def("IndexGraph")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("IndexGraph", + "IndexGraph[g] renames the vertices of g to consecutive integers from 1 " + "(IndexGraph[g, k] from k), remapping edges."); + + symtab_add_builtin("EmptyGraphQ", builtin_empty_graph_q); + symtab_get_def("EmptyGraphQ")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("EmptyGraphQ", + "EmptyGraphQ[g] gives True iff g has no edges."); + + symtab_add_builtin("MixedGraphQ", builtin_mixed_graph_q); + symtab_get_def("MixedGraphQ")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("MixedGraphQ", + "MixedGraphQ[g] gives True iff g has both directed and undirected edges."); + + symtab_add_builtin("GraphProduct", builtin_graph_product); + symtab_get_def("GraphProduct")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("GraphProduct", + "GraphProduct[g1, g2, type] gives a product graph on V1 x V2 for type " + "\"Cartesian\", \"Tensor\", \"Strong\", or \"Lexicographic\"."); + + symtab_add_builtin("TuranGraph", builtin_turan_graph); + symtab_get_def("TuranGraph")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("TuranGraph", + "TuranGraph[n, r] gives the Turan graph: the balanced complete r-partite " + "graph on n vertices (the largest (r+1)-clique-free graph)."); + + symtab_add_builtin("CompleteKaryTree", builtin_complete_kary_tree); + symtab_get_def("CompleteKaryTree")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("CompleteKaryTree", + "CompleteKaryTree[L] / CompleteKaryTree[L, k] gives the complete k-ary " + "tree with L levels (k=2 by default)."); + + symtab_add_builtin("CirculantGraph", builtin_circulant_graph); + symtab_get_def("CirculantGraph")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("CirculantGraph", + "CirculantGraph[n, {j1, ...}] gives the circulant graph on n vertices " + "joining vertex i to i +/- jk (mod n) for each jump jk."); + + symtab_add_builtin("LadderGraph", builtin_ladder_graph); + symtab_get_def("LadderGraph")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("LadderGraph", + "LadderGraph[n] gives the ladder graph: two n-vertex paths joined by n " + "rungs (the product P_n x P_2)."); + + symtab_add_builtin("CocktailPartyGraph", builtin_cocktail_party_graph); + symtab_get_def("CocktailPartyGraph")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("CocktailPartyGraph", + "CocktailPartyGraph[n] gives the cocktail-party graph K_{n x 2}: 2n " + "vertices in n couples, each joined to all but its partner."); + + symtab_add_builtin("KneserGraph", builtin_kneser_graph); + symtab_get_def("KneserGraph")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("KneserGraph", + "KneserGraph[n, k] gives the Kneser graph: k-subsets of {1..n} as " + "vertices, adjacent iff disjoint (K(5,2) is the Petersen graph)."); + + symtab_add_builtin("GeneralizedPetersenGraph", builtin_generalized_petersen_graph); + symtab_get_def("GeneralizedPetersenGraph")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("GeneralizedPetersenGraph", + "GeneralizedPetersenGraph[n, k] gives GP(n,k): an outer n-cycle, an inner " + "star polygon {n/k}, and spokes joining them (GP(5,2) is the Petersen graph)."); + + symtab_add_builtin("FriendshipGraph", builtin_friendship_graph); + symtab_get_def("FriendshipGraph")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("FriendshipGraph", + "FriendshipGraph[n] gives the windmill graph of n triangles sharing one " + "central vertex (F_1 is a triangle, F_2 the bowtie)."); + + symtab_add_builtin("VertexCoreness", builtin_vertex_coreness); + symtab_get_def("VertexCoreness")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("VertexCoreness", + "VertexCoreness[g] gives, for each vertex, its coreness: the largest k " + "such that it lies in the k-core of g."); + + symtab_add_builtin("TransitiveReductionGraph", builtin_transitive_reduction_graph); + symtab_get_def("TransitiveReductionGraph")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("TransitiveReductionGraph", + "TransitiveReductionGraph[g] gives the transitive reduction of a directed " + "acyclic graph: the fewest-edge graph with the same reachability."); + + symtab_add_builtin("Subgraph", builtin_subgraph); + symtab_get_def("Subgraph")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("Subgraph", + "Subgraph[g, {v1, ...}] gives the subgraph of g induced by the listed " + "vertices (edges with both endpoints among them)."); + + symtab_add_builtin("VertexDelete", builtin_vertex_delete); + symtab_get_def("VertexDelete")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("VertexDelete", + "VertexDelete[g, v] (or VertexDelete[g, {v1, ...}]) removes the given " + "vertices and all incident edges from g."); + + symtab_add_builtin("EdgeDelete", builtin_edge_delete); + symtab_get_def("EdgeDelete")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("EdgeDelete", + "EdgeDelete[g, e] (or EdgeDelete[g, {e1, ...}]) removes the given edges " + "from g, keeping all vertices."); + + symtab_add_builtin("EdgeAdd", builtin_edge_add); + symtab_get_def("EdgeAdd")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("EdgeAdd", + "EdgeAdd[g, e] (or EdgeAdd[g, {e1, ...}]) adds the given edges to g, " + "introducing any missing endpoints as new vertices."); + + symtab_add_builtin("VertexAdd", builtin_vertex_add); + symtab_get_def("VertexAdd")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("VertexAdd", + "VertexAdd[g, v] (or VertexAdd[g, {v1, ...}]) adds the given vertices to " + "g as isolated vertices, leaving edges unchanged."); + + symtab_add_builtin("NeighborhoodGraph", builtin_neighborhood_graph); + symtab_get_def("NeighborhoodGraph")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("NeighborhoodGraph", + "NeighborhoodGraph[g, v] (or [g, v, k]) gives the subgraph induced by v " + "and all vertices within distance k (k=1 by default)."); + + symtab_add_builtin("GraphDisjointUnion", builtin_graph_disjoint_union); + symtab_get_def("GraphDisjointUnion")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("GraphDisjointUnion", + "GraphDisjointUnion[g1, g2] gives the disjoint union of g1 and g2 " + "(vertices relabeled 1..n1+n2, no edges between the two blocks)."); + + symtab_add_builtin("EdgeContract", builtin_edge_contract); + symtab_get_def("EdgeContract")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("EdgeContract", + "EdgeContract[g, e] contracts edge e, merging its endpoints into one " + "vertex, dropping the self-loop, and collapsing parallel edges."); + + symtab_add_builtin("FindIndependentEdgeSet", builtin_find_independent_edge_set); + symtab_get_def("FindIndependentEdgeSet")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("FindIndependentEdgeSet", + "FindIndependentEdgeSet[g] gives a maximum matching of g: a largest set " + "of edges no two of which share a vertex."); + + symtab_add_builtin("FindDominatingSet", builtin_find_dominating_set); + symtab_get_def("FindDominatingSet")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("FindDominatingSet", + "FindDominatingSet[g] gives a minimum dominating set: a smallest set of " + "vertices such that every vertex is in it or adjacent to it."); + + symtab_add_builtin("FindEdgeCover", builtin_find_edge_cover); + symtab_get_def("FindEdgeCover")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("FindEdgeCover", + "FindEdgeCover[g] gives a minimum edge cover: a smallest set of edges " + "such that every vertex is incident to at least one ({} if none exists)."); + + symtab_add_builtin("FindVertexColoring", builtin_find_vertex_coloring); + symtab_get_def("FindVertexColoring")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("FindVertexColoring", + "FindVertexColoring[g] gives a proper coloring of g using the fewest " + "colors, as a list of color indices (one per vertex)."); + + symtab_add_builtin("GraphAssortativity", builtin_graph_assortativity); + symtab_get_def("GraphAssortativity")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("GraphAssortativity", + "GraphAssortativity[g] gives the degree assortativity coefficient of g " + "(exact rational in [-1,1]; Indeterminate for a regular graph)."); + + symtab_add_builtin("IncidenceList", builtin_incidence_list); + symtab_get_def("IncidenceList")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("IncidenceList", + "IncidenceList[g, v] gives the list of edges of g incident to vertex v."); + + symtab_add_builtin("VertexOutComponent", builtin_vertex_out_component); + symtab_get_def("VertexOutComponent")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("VertexOutComponent", + "VertexOutComponent[g, v] gives the vertices reachable from v (including " + "v) following edge directions."); + + symtab_add_builtin("VertexInComponent", builtin_vertex_in_component); + symtab_get_def("VertexInComponent")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("VertexInComponent", + "VertexInComponent[g, v] gives the vertices from which v is reachable " + "(including v)."); + + symtab_add_builtin("AntiprismGraph", builtin_antiprism_graph); + symtab_get_def("AntiprismGraph")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("AntiprismGraph", + "AntiprismGraph[n] gives the n-antiprism: two offset n-cycles with cross " + "edges (4-regular, 4n edges; n=3 is the octahedron)."); + + symtab_add_builtin("PrismGraph", builtin_prism_graph); + symtab_get_def("PrismGraph")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("PrismGraph", + "PrismGraph[n] gives the n-gonal prism: two n-cycles joined by rungs " + "(3-regular, 3n edges; n=4 is the cube)."); + + symtab_add_builtin("SunletGraph", builtin_sunlet_graph); + symtab_get_def("SunletGraph")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("SunletGraph", + "SunletGraph[n] gives the n-sunlet: a cycle C_n with a pendant vertex " + "attached to each cycle vertex (2n vertices, 2n edges)."); + + symtab_add_builtin("HelmGraph", builtin_helm_graph); + symtab_get_def("HelmGraph")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("HelmGraph", + "HelmGraph[n] gives the helm graph: a wheel on an n-cycle rim with a " + "pendant vertex attached to each rim vertex (2n+1 vertices, 3n edges)."); + + symtab_add_builtin("GearGraph", builtin_gear_graph); + symtab_get_def("GearGraph")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("GearGraph", + "GearGraph[n] gives the gear graph: a hub joined to alternate vertices of " + "a 2n-cycle (2n+1 vertices, 3n edges; bipartite)."); + + symtab_add_builtin("EdgeBetweennessCentrality", builtin_edge_betweenness_centrality); + symtab_get_def("EdgeBetweennessCentrality")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("EdgeBetweennessCentrality", + "EdgeBetweennessCentrality[g] gives, for each edge, the number of " + "shortest paths through it (exact rationals when paths tie)."); + + symtab_add_builtin("DodecahedralGraph", builtin_dodecahedral_graph); + symtab_get_def("DodecahedralGraph")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("DodecahedralGraph", + "DodecahedralGraph[] gives the dodecahedron graph (GP(10,2)): 20 " + "vertices, 30 edges, 3-regular."); + + symtab_add_builtin("IcosahedralGraph", builtin_icosahedral_graph); + symtab_get_def("IcosahedralGraph")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("IcosahedralGraph", + "IcosahedralGraph[] gives the icosahedron graph: 12 vertices, 30 edges, " + "5-regular."); + + /* ---- Phase 6: visualization ------------------------------------------ */ + symtab_add_builtin("GraphPlot", builtin_graph_plot); + symtab_get_def("GraphPlot")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("GraphPlot", + "GraphPlot[g] gives a Graphics object drawing the graph g. Options: " + "GraphLayout->\"name\" (e.g. \"SpringElectricalEmbedding\", " + "\"CircularEmbedding\", \"SpiralEmbedding\", \"LinearEmbedding\", " + "\"GridEmbedding\", \"RadialEmbedding\", \"LayeredEmbedding\", " + "\"BipartiteEmbedding\", \"StarEmbedding\", \"RandomEmbedding\"), " + "VertexStyle->color, EdgeStyle->color, VertexSize->r, " + "VertexLabels->None. A bare Graph also auto-renders with these " + "defaults."); + + symtab_add_builtin("Graph3D", builtin_graph3d); + symtab_get_def("Graph3D")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("Graph3D", + "Graph3D[v, e] / Graph3D[e] builds a graph like Graph but displays it as " + "a 3D node-link diagram (force-directed layout in a cube). Same edge " + "sugar (u->v, u<->v) and simple-graph rules as Graph."); + + symtab_add_builtin("HighlightGraph", builtin_highlight_graph); + symtab_get_def("HighlightGraph")->attributes |= ATTR_PROTECTED; + symtab_set_docstring("HighlightGraph", + "HighlightGraph[g, parts] draws g with the given vertices and/or edges " + "emphasized (accent color, rest dimmed). Each part may be a vertex, an " + "edge (u<->v / u->v), or a list of vertices treated as a path " + "(highlighting its vertices and joining edges). Returns a Graphics."); +} diff --git a/src/graph/graph.h b/src/graph/graph.h new file mode 100644 index 00000000..ce75ebd5 --- /dev/null +++ b/src/graph/graph.h @@ -0,0 +1,259 @@ +#ifndef GRAPH_H +#define GRAPH_H + +#include "expr.h" + +/* Graph subsystem entry points, registered by graph_init(). + * + * Graphs are represented as ordinary Expr trees -- no new EXPR_* tag: + * + * Graph[ List[v1, v2, ...], List[edge1, edge2, ...] ] + * + * where each edge is DirectedEdge[u, v] or UndirectedEdge[u, v]. Rule/-> + * and TwoWayRule/<-> are accepted as parse-time sugar and normalized on + * construction. Vertices are arbitrary expressions. + * + * This mirrors the src/linalg/ layout: one builtin per translation unit, + * with the builtin_* prototypes declared here and registered in graph.c. + * The set of prototypes grows phase by phase as builtins land. + * + * Ownership follows the SPEC §4 contract: a builtin takes ownership of its + * argument `res`, returns a new Expr* on success (the evaluator frees res), + * or NULL to leave the expression unevaluated (the evaluator retains res). + */ + +/* Graph[...] construction: normalizes edge sugar (Rule/TwoWayRule -> + * Directed/UndirectedEdge), derives vertices when omitted, validates, and + * returns the canonical Graph[List[verts], List[edges]]. Returns NULL to + * leave malformed input (self-loops, parallel edges, 3-arg edges, unknown + * edge endpoints) unevaluated, and also NULL when the argument is already + * canonical (so evaluation reaches a fixed point). */ +Expr* builtin_graph(Expr* res); + +/* GraphQ[g]: True iff g is a canonical, valid graph; False otherwise. */ +Expr* builtin_graph_q(Expr* res); + +/* Module initializer: registers builtins, attributes, and docstrings. + * Called from core_init() in src/core.c. */ +void graph_init(void); + +/* ---- Shared helpers (src/graph/graph_util.c) ------------------------------ + * Used across the subsystem (constructor, predicates, printer, and later the + * query/matrix/algorithm builtins). All are read-only over their arguments. */ + +/* True iff e is a List[...] function node. */ +int graph_is_list(const Expr* e); + +/* If e is a normalized 2-argument edge, returns the interned edge-head pointer + * (SYM_DirectedEdge or SYM_UndirectedEdge); otherwise NULL. */ +const char* graph_edge_kind(const Expr* e); + +/* True iff g is a canonical, valid graph: Graph[List verts, List edges] where + * every edge is a 2-arg DirectedEdge/UndirectedEdge, there are no self-loops, + * no parallel/duplicate edges, and every edge endpoint appears in verts. */ +int graph_is_valid(const Expr* g); + +/* Same validity check but for an arbitrary outer head (Graph or Graph3D). */ +int graph_is_valid_head(const Expr* g, const char* head_sym); + +/* Index of vertex v within List `verts` (linear expr_eq scan), or -1. */ +int graph_vertex_index(const Expr* verts, const Expr* v); + +/* ---- Phase 2: query / representation builtins ----------------------------- */ +Expr* builtin_vertex_list(Expr* res); /* VertexList[g] */ +Expr* builtin_edge_list(Expr* res); /* EdgeList[g] */ +Expr* builtin_vertex_count(Expr* res); /* VertexCount[g] */ +Expr* builtin_edge_count(Expr* res); /* EdgeCount[g] */ +Expr* builtin_adjacency_list(Expr* res); /* AdjacencyList[g] / [g,v] */ +Expr* builtin_vertex_degree(Expr* res); /* VertexDegree[g] / [g,v] */ +Expr* builtin_vertex_in_degree(Expr* res); /* VertexInDegree[g] / [g,v] */ +Expr* builtin_vertex_out_degree(Expr* res);/* VertexOutDegree[g] / [g,v] */ +Expr* builtin_directed_graph_q(Expr* res); /* DirectedGraphQ[g] */ + +/* ---- Phase 3: matrix views (linalg interop) ------------------------------- */ +Expr* builtin_adjacency_matrix(Expr* res); /* AdjacencyMatrix[g] */ +Expr* builtin_incidence_matrix(Expr* res); /* IncidenceMatrix[g] */ +Expr* builtin_adjacency_graph(Expr* res); /* AdjacencyGraph[m] */ + +/* ---- Phase 4: graph generators -------------------------------------------- */ +Expr* builtin_complete_graph(Expr* res); /* CompleteGraph[n] */ +Expr* builtin_cycle_graph(Expr* res); /* CycleGraph[n] */ +Expr* builtin_path_graph(Expr* res); /* PathGraph[n] / PathGraph[{...}] */ +Expr* builtin_random_graph(Expr* res); /* RandomGraph[{n, m}] */ +Expr* builtin_star_graph(Expr* res); /* StarGraph[n] */ +Expr* builtin_wheel_graph(Expr* res); /* WheelGraph[n] */ +Expr* builtin_grid_graph(Expr* res); /* GridGraph[{d1,d2,...}] */ +Expr* builtin_hypercube_graph(Expr* res); /* HypercubeGraph[k] */ + +/* ---- Phase 5: shared adjacency scaffolding (graph_util.c) ------------------ + * Integer-indexed adjacency derived from a validated graph. Vertex i is + * verts[i] (canonical order). `out`/`in` hold successor/predecessor indices; + * an UndirectedEdge{a,b} contributes symmetrically to both, so the underlying + * undirected neighborhood of v is out[v] together with in[v]. Built on demand + * per algorithm call (linear expr_eq indexing; documented upgrade path is an + * expr_hash map). The caller owns the result and frees it with graph_adj_free. */ +typedef struct GraphAdj { + int n; /* number of vertices */ + const Expr* verts; /* borrowed: the vertex List of the source graph */ + int* outdeg; int** out; /* successors: out[i][0..outdeg[i]-1] */ + int* indeg; int** in; /* predecessors: in[i][0..indeg[i]-1] */ +} GraphAdj; + +GraphAdj* graph_build_adj(const Expr* g); /* NULL if g is not a valid graph */ +void graph_adj_free(GraphAdj* a); + +/* Count connected components of the underlying undirected graph, considering + * only vertices with removed[i]==0 (removed may be NULL = none removed). Writes + * the number of active vertices to *active_out when non-NULL. */ +int graph_count_components(const GraphAdj* a, const char* removed, int* active_out); + +/* ---- Phase 5: search & computation builtins ------------------------------- */ +Expr* builtin_find_shortest_path(Expr* res); /* FindShortestPath[g,s,t] */ +Expr* builtin_graph_distance(Expr* res); /* GraphDistance[g,s,t] */ +Expr* builtin_connected_components(Expr* res); /* ConnectedComponents */ +Expr* builtin_weakly_connected_components(Expr* res); /* Weakly... */ +Expr* builtin_strongly_connected_components(Expr* res); /* Strongly... */ +Expr* builtin_find_spanning_tree(Expr* res); /* FindSpanningTree */ +Expr* builtin_connected_graph_q(Expr* res); /* ConnectedGraphQ */ +Expr* builtin_vertex_connectivity(Expr* res); /* VertexConnectivity */ +Expr* builtin_bipartite_graph_q(Expr* res); /* BipartiteGraphQ */ +Expr* builtin_vertex_eccentricity(Expr* res); /* VertexEccentricity */ +Expr* builtin_graph_diameter(Expr* res); /* GraphDiameter */ +Expr* builtin_graph_radius(Expr* res); /* GraphRadius */ +Expr* builtin_graph_center(Expr* res); /* GraphCenter */ +Expr* builtin_graph_periphery(Expr* res); /* GraphPeriphery */ +Expr* builtin_acyclic_graph_q(Expr* res); /* AcyclicGraphQ */ +Expr* builtin_topological_sort(Expr* res); /* TopologicalSort */ +Expr* builtin_graph_complement(Expr* res); /* GraphComplement */ +Expr* builtin_kirchhoff_matrix(Expr* res); /* KirchhoffMatrix */ +Expr* builtin_edge_connectivity(Expr* res); /* EdgeConnectivity */ +Expr* builtin_line_graph(Expr* res); /* LineGraph */ +Expr* builtin_eulerian_graph_q(Expr* res); /* EulerianGraphQ */ +Expr* builtin_closeness_centrality(Expr* res); /* ClosenessCentrality */ +Expr* builtin_transitive_closure(Expr* res); /* TransitiveClosure */ +Expr* builtin_betweenness_centrality(Expr* res); /* BetweennessCentrality*/ +Expr* builtin_find_eulerian_cycle(Expr* res); /* FindEulerianCycle */ +Expr* builtin_find_hamiltonian_cycle(Expr* res); /* FindHamiltonianCycle */ +Expr* builtin_graph_power(Expr* res); /* GraphPower[g, k] */ +Expr* builtin_find_cycle(Expr* res); /* FindCycle[g] */ +Expr* builtin_graph_distance_matrix(Expr* res); /* GraphDistanceMatrix */ +Expr* builtin_graph_density(Expr* res); /* GraphDensity[g] */ +Expr* builtin_degree_centrality(Expr* res); /* DegreeCentrality[g] */ +Expr* builtin_find_hamiltonian_path(Expr* res); /* FindHamiltonianPath */ +Expr* builtin_kcore_components(Expr* res); /* KCoreComponents[g,k] */ +Expr* builtin_local_clustering_coefficient(Expr* res); /* LocalClusteringCoeff */ +Expr* builtin_global_clustering_coefficient(Expr* res); /* GlobalClusteringCoeff*/ +Expr* builtin_mean_clustering_coefficient(Expr* res); /* MeanClusteringCoeff */ +Expr* builtin_find_clique(Expr* res); /* FindClique[g] */ +Expr* builtin_find_independent_vertex_set(Expr* res); /* FindIndependentVertexSet */ +Expr* builtin_find_vertex_cover(Expr* res); /* FindVertexCover[g] */ +Expr* builtin_graph_reciprocity(Expr* res); /* GraphReciprocity[g] */ +Expr* builtin_chromatic_polynomial(Expr* res); /* ChromaticPolynomial */ +Expr* builtin_chromatic_number(Expr* res); /* ChromaticNumber[g] */ +Expr* builtin_degree_sequence(Expr* res); /* DegreeSequence[g] */ +Expr* builtin_tree_graph_q(Expr* res); /* TreeGraphQ[g] */ +Expr* builtin_strongly_connected_graph_q(Expr* res); /* StronglyConnectedGraphQ */ +Expr* builtin_hamiltonian_graph_q(Expr* res); /* HamiltonianGraphQ[g] */ +Expr* builtin_regular_graph_q(Expr* res); /* RegularGraphQ[g] */ +Expr* builtin_complete_graph_q(Expr* res); /* CompleteGraphQ[g] */ +Expr* builtin_graph_union(Expr* res); /* GraphUnion[g1, g2] */ +Expr* builtin_graph_intersection(Expr* res); /* GraphIntersection */ +Expr* builtin_graph_difference(Expr* res); /* GraphDifference */ +Expr* builtin_graph_reverse(Expr* res); /* ReverseGraph[g] */ +Expr* builtin_path_graph_q(Expr* res); /* PathGraphQ[g] */ +Expr* builtin_vertex_contract(Expr* res); /* VertexContract[g,vs] */ +Expr* builtin_pagerank_centrality(Expr* res); /* PageRankCentrality */ +Expr* builtin_katz_centrality(Expr* res); /* KatzCentrality[g,a] */ +Expr* builtin_graph_join(Expr* res); /* GraphJoin[g1, g2] */ +Expr* builtin_index_graph(Expr* res); /* IndexGraph[g] / [g,k] */ +Expr* builtin_empty_graph_q(Expr* res); /* EmptyGraphQ[g] */ +Expr* builtin_mixed_graph_q(Expr* res); /* MixedGraphQ[g] */ +Expr* builtin_graph_product(Expr* res); /* GraphProduct[g1,g2,t] */ +Expr* builtin_turan_graph(Expr* res); /* TuranGraph[n, r] */ +Expr* builtin_complete_kary_tree(Expr* res); /* CompleteKaryTree[L,k] */ +Expr* builtin_circulant_graph(Expr* res); /* CirculantGraph[n,js] */ +Expr* builtin_ladder_graph(Expr* res); /* LadderGraph[n] */ +Expr* builtin_cocktail_party_graph(Expr* res); /* CocktailPartyGraph[n] */ +Expr* builtin_kneser_graph(Expr* res); /* KneserGraph[n, k] */ +Expr* builtin_generalized_petersen_graph(Expr* res); /* GeneralizedPetersenGraph */ +Expr* builtin_friendship_graph(Expr* res); /* FriendshipGraph[n] */ +Expr* builtin_vertex_coreness(Expr* res); /* VertexCoreness[g] */ +Expr* builtin_transitive_reduction_graph(Expr* res); /* TransitiveReductionGraph */ +Expr* builtin_subgraph(Expr* res); /* Subgraph[g, {verts}] */ +Expr* builtin_vertex_delete(Expr* res); /* VertexDelete[g, v] */ +Expr* builtin_edge_delete(Expr* res); /* EdgeDelete[g, e] */ +Expr* builtin_edge_add(Expr* res); /* EdgeAdd[g, e] */ +Expr* builtin_vertex_add(Expr* res); /* VertexAdd[g, v] */ +Expr* builtin_neighborhood_graph(Expr* res); /* NeighborhoodGraph */ +Expr* builtin_graph_disjoint_union(Expr* res); /* GraphDisjointUnion */ +Expr* builtin_edge_contract(Expr* res); /* EdgeContract[g, e] */ +Expr* builtin_find_independent_edge_set(Expr* res); /* FindIndependentEdgeSet */ +Expr* builtin_find_dominating_set(Expr* res); /* FindDominatingSet[g] */ +Expr* builtin_find_edge_cover(Expr* res); /* FindEdgeCover[g] */ +Expr* builtin_find_vertex_coloring(Expr* res); /* FindVertexColoring */ +Expr* builtin_graph_assortativity(Expr* res); /* GraphAssortativity */ +Expr* builtin_incidence_list(Expr* res); /* IncidenceList[g, v] */ +Expr* builtin_vertex_out_component(Expr* res); /* VertexOutComponent */ +Expr* builtin_vertex_in_component(Expr* res); /* VertexInComponent */ +Expr* builtin_antiprism_graph(Expr* res); /* AntiprismGraph[n] */ +Expr* builtin_prism_graph(Expr* res); /* PrismGraph[n] */ +Expr* builtin_sunlet_graph(Expr* res); /* SunletGraph[n] */ +Expr* builtin_helm_graph(Expr* res); /* HelmGraph[n] */ +Expr* builtin_gear_graph(Expr* res); /* GearGraph[n] */ +Expr* builtin_edge_betweenness_centrality(Expr* res); /* EdgeBetweennessCentrality */ +Expr* builtin_dodecahedral_graph(Expr* res); /* DodecahedralGraph[] */ +Expr* builtin_icosahedral_graph(Expr* res); /* IcosahedralGraph[] */ + +/* ---- Phase 6: visualization ----------------------------------------------- */ +Expr* builtin_graph_plot(Expr* res); /* GraphPlot[g] -> Graphics[...] */ +Expr* builtin_highlight_graph(Expr* res); /* HighlightGraph[g, parts] */ + +/* Vertex-coordinate layout (src/graph/layout.c). Fills caller-allocated x[]/y[] + * (length = VertexCount[g]) with 2D coordinates under the named layout, + * normalized to the [-1,1] box. `layout` is a Wolfram GraphLayout name (e.g. + * "SpringElectricalEmbedding"); NULL / unknown falls back to circular. Fully + * deterministic so notebooks reproduce. Read-only over g. */ +void graph_compute_layout(const Expr* g, const char* layout, double* x, double* y); + +/* Styling passed to graph_render(). Colors are borrowed RGBColor expressions + * (or NULL for the built-in defaults). Highlight masks, when non-NULL, are + * arrays as long as the vertex / edge list: a set entry draws that element in + * the accent color and dims the rest. */ +typedef struct GraphStyle { + const char* layout; /* layout name, or NULL for circular */ + const Expr* vertex_color; /* RGBColor for all vertices, or NULL */ + const Expr* edge_color; /* RGBColor for all edges, or NULL */ + double vertex_size; /* Disk radius; <= 0 uses the default */ + int show_labels; /* draw Text vertex labels (default 1) */ + const char* hi_vert; /* per-vertex highlight mask, or NULL */ + const char* hi_edge; /* per-edge highlight mask, or NULL */ +} GraphStyle; + +/* Render a validated graph to a Graphics[...] expression using `st` (NULL = all + * defaults). Circular layout, blue vertices, labelled. Caller owns the result; + * read-only over g and st. */ +Expr* graph_render(const Expr* g, const GraphStyle* st); + +/* Convenience wrapper: graph_render with all defaults. Used by the REPL to + * auto-display a bare Graph result as a diagram. */ +Expr* graph_default_graphics(const Expr* g); + +/* ---- Graph3D: three-dimensional graphs ------------------------------------ */ + +/* Graph3D[...] — same construction/normalization as Graph, but the canonical + * value has head Graph3D and auto-displays as a 3D node-link diagram. */ +Expr* builtin_graph3d(Expr* res); + +/* 3D vertex layout (src/graph/layout.c): fills caller-allocated x[]/y[]/z[] + * (length = VertexCount) with coordinates in the [-1,1] cube. `layout` selects + * a kernel (currently a 3D Fruchterman-Reingold spring, seeded on a sphere; + * "SphericalEmbedding" places vertices on the sphere). Deterministic. */ +void graph_compute_layout3d(const Expr* g, const char* layout, + double* x, double* y, double* z); + +/* Render a valid Graph3D to a Graphics3D[...] expression (edges as 3D Lines, + * vertices as a Point set). Caller owns the result. */ +Expr* graph_render3d(const Expr* g, const GraphStyle* st); +Expr* graph_default_graphics3d(const Expr* g); + +#endif /* GRAPH_H */ diff --git a/src/graph/graph_util.c b/src/graph/graph_util.c new file mode 100644 index 00000000..a396737f --- /dev/null +++ b/src/graph/graph_util.c @@ -0,0 +1,194 @@ +/* graph_util.c - shared, read-only helpers for the graph subsystem. + * + * Graphs are ordinary Expr trees; these helpers inspect the canonical form + * + * Graph[ List[v1, ...], List[edge1, ...] ] + * + * where each edge is a 2-argument DirectedEdge[u, v] or UndirectedEdge[u, v]. + * Nothing here allocates or mutates; ownership contracts live in the callers. + * + * The MVP vertex-membership test is a linear expr_eq scan (O(V) per lookup). + * That is fine for pico-CAS graph sizes; an expr_hash-based index is the + * documented upgrade path when profiling warrants it. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +/* True iff e is a function node whose head is the interned symbol `sym`. */ +static int head_is_sym(const Expr* e, const char* sym) { + return e && e->type == EXPR_FUNCTION + && e->data.function.head + && e->data.function.head->type == EXPR_SYMBOL + && e->data.function.head->data.symbol == sym; +} + +int graph_is_list(const Expr* e) { + return head_is_sym(e, SYM_List); +} + +const char* graph_edge_kind(const Expr* e) { + if (!e || e->type != EXPR_FUNCTION || e->data.function.arg_count != 2) + return NULL; + if (head_is_sym(e, SYM_DirectedEdge)) return SYM_DirectedEdge; + if (head_is_sym(e, SYM_UndirectedEdge)) return SYM_UndirectedEdge; + return NULL; +} + +/* True iff `v` is structurally equal to some element of List `list`. */ +static int vertex_in_list(const Expr* list, const Expr* v) { + for (size_t i = 0; i < list->data.function.arg_count; i++) { + if (expr_eq(list->data.function.args[i], v)) return 1; + } + return 0; +} + +int graph_vertex_index(const Expr* verts, const Expr* v) { + if (!graph_is_list(verts)) return -1; + for (size_t i = 0; i < verts->data.function.arg_count; i++) { + if (expr_eq(verts->data.function.args[i], v)) return (int)i; + } + return -1; +} + +/* ---- Phase 5: adjacency scaffolding --------------------------------------- */ + +void graph_adj_free(GraphAdj* a) { + if (!a) return; + for (int i = 0; i < a->n; i++) { free(a->out[i]); free(a->in[i]); } + free(a->out); free(a->in); + free(a->outdeg); free(a->indeg); + free(a); +} + +GraphAdj* graph_build_adj(const Expr* g) { + if (!graph_is_valid(g)) return NULL; + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + int n = (int)verts->data.function.arg_count; + size_t ne = edges->data.function.arg_count; + + GraphAdj* a = calloc(1, sizeof(GraphAdj)); + if (!a) return NULL; + a->n = n; + a->verts = verts; + a->outdeg = calloc((size_t)(n > 0 ? n : 1), sizeof(int)); + a->indeg = calloc((size_t)(n > 0 ? n : 1), sizeof(int)); + a->out = calloc((size_t)(n > 0 ? n : 1), sizeof(int*)); + a->in = calloc((size_t)(n > 0 ? n : 1), sizeof(int*)); + + /* Pass 1: count degrees. */ + for (size_t k = 0; k < ne; k++) { + const Expr* e = edges->data.function.args[k]; + const char* kind = graph_edge_kind(e); + int ia = graph_vertex_index(verts, e->data.function.args[0]); + int ib = graph_vertex_index(verts, e->data.function.args[1]); + a->outdeg[ia]++; a->indeg[ib]++; + if (kind == SYM_UndirectedEdge) { a->outdeg[ib]++; a->indeg[ia]++; } + } + for (int i = 0; i < n; i++) { + a->out[i] = (a->outdeg[i] > 0) ? calloc((size_t)a->outdeg[i], sizeof(int)) : NULL; + a->in[i] = (a->indeg[i] > 0) ? calloc((size_t)a->indeg[i], sizeof(int)) : NULL; + } + + /* Pass 2: fill (reuse degree counters as write cursors). */ + int* oc = calloc((size_t)(n > 0 ? n : 1), sizeof(int)); + int* ic = calloc((size_t)(n > 0 ? n : 1), sizeof(int)); + for (size_t k = 0; k < ne; k++) { + const Expr* e = edges->data.function.args[k]; + const char* kind = graph_edge_kind(e); + int ia = graph_vertex_index(verts, e->data.function.args[0]); + int ib = graph_vertex_index(verts, e->data.function.args[1]); + a->out[ia][oc[ia]++] = ib; a->in[ib][ic[ib]++] = ia; + if (kind == SYM_UndirectedEdge) { + a->out[ib][oc[ib]++] = ia; a->in[ia][ic[ia]++] = ib; + } + } + free(oc); free(ic); + return a; +} + +int graph_count_components(const GraphAdj* a, const char* removed, int* active_out) { + int n = a->n; + char* seen = calloc((size_t)(n > 0 ? n : 1), sizeof(char)); + int* stack = calloc((size_t)(n > 0 ? n : 1), sizeof(int)); + int comps = 0, active = 0; + + for (int s = 0; s < n; s++) { + if (removed && removed[s]) continue; + active++; + if (seen[s]) continue; + /* New component: DFS over underlying undirected neighbors (out + in). */ + comps++; + int top = 0; stack[top++] = s; seen[s] = 1; + while (top > 0) { + int u = stack[--top]; + for (int j = 0; j < a->outdeg[u]; j++) { + int w = a->out[u][j]; + if ((removed && removed[w]) || seen[w]) continue; + seen[w] = 1; stack[top++] = w; + } + for (int j = 0; j < a->indeg[u]; j++) { + int w = a->in[u][j]; + if ((removed && removed[w]) || seen[w]) continue; + seen[w] = 1; stack[top++] = w; + } + } + } + free(seen); free(stack); + if (active_out) *active_out = active; + return comps; +} + +/* Two normalized edges are "parallel" (duplicates) when they connect the same + * endpoints in a way the graph cannot distinguish: + * - directed: same head and same ordered (u, v); + * - undirected: same head and the same unordered pair {u, v}. + * Directed a->b and b->a are distinct; that is allowed. */ +static int edges_parallel(const Expr* e1, const Expr* e2) { + const char* k1 = graph_edge_kind(e1); + const char* k2 = graph_edge_kind(e2); + if (!k1 || k1 != k2) return 0; + const Expr* a1 = e1->data.function.args[0]; + const Expr* b1 = e1->data.function.args[1]; + const Expr* a2 = e2->data.function.args[0]; + const Expr* b2 = e2->data.function.args[1]; + if (expr_eq(a1, a2) && expr_eq(b1, b2)) return 1; + if (k1 == SYM_UndirectedEdge && expr_eq(a1, b2) && expr_eq(b1, a2)) return 1; + return 0; +} + +int graph_is_valid_head(const Expr* g, const char* head_sym) { + if (!head_is_sym(g, head_sym) || g->data.function.arg_count != 2) + return 0; + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + if (!graph_is_list(verts) || !graph_is_list(edges)) return 0; + + size_t ne = edges->data.function.arg_count; + for (size_t i = 0; i < ne; i++) { + const Expr* edge = edges->data.function.args[i]; + if (!graph_edge_kind(edge)) return 0; /* un-normalized/3-arg */ + + const Expr* u = edge->data.function.args[0]; + const Expr* v = edge->data.function.args[1]; + if (expr_eq(u, v)) return 0; /* self-loop */ + if (!vertex_in_list(verts, u)) return 0; /* endpoint not a vertex */ + if (!vertex_in_list(verts, v)) return 0; + + for (size_t j = 0; j < i; j++) { + if (edges_parallel(edge, edges->data.function.args[j])) return 0; + } + } + return 1; +} + +int graph_is_valid(const Expr* g) { + /* Both 2D Graph and 3D Graph3D count as valid graphs, so every query, + * matrix, and algorithm builtin works transparently on a Graph3D. */ + return graph_is_valid_head(g, SYM_Graph) + || graph_is_valid_head(g, SYM_Graph3D); +} diff --git a/src/graph/graphassortativity.c b/src/graph/graphassortativity.c new file mode 100644 index 00000000..a56a172f --- /dev/null +++ b/src/graph/graphassortativity.c @@ -0,0 +1,67 @@ +/* graphassortativity.c - GraphAssortativity[g]: the degree assortativity + * coefficient of g -- the Pearson correlation between the degrees of the two + * endpoints of an edge, in [-1, 1]. It is +1 when high-degree vertices attach to + * high-degree ones and -1 when they attach to low-degree ones. + * + * Newman's edge-list form reduces to a ratio of integers, so the result is an + * exact rational. Over the M undirected edges, with j,k the endpoint degrees, let + * A = sum j*k, S1 = sum (j+k), Sq = sum (j^2 + k^2). + * Then r = (4*M*A - S1^2) / (2*M*Sq - S1^2). The denominator is the degree + * variance times a positive factor; it is 0 exactly when g is regular (all + * endpoint degrees equal), in which case the coefficient is Indeterminate. + * + * Edge direction is ignored (computed on the underlying undirected graph). A star + * is perfectly disassortative (-1). Memory (SPEC section 4): returns a fresh + * number; frees res. NULL on a non-graph. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include "eval.h" +#include + +static Expr* ratio(long num, long den) { + Expr* pa[2] = { expr_new_integer(den), expr_new_integer(-1) }; + Expr* inv = expr_new_function(expr_new_symbol(SYM_Power), pa, 2); + Expr* ta[2] = { expr_new_integer(num), inv }; + return evaluate(expr_new_function(expr_new_symbol(SYM_Times), ta, 2)); +} + +Expr* builtin_graph_assortativity(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + int n = (int)verts->data.function.arg_count; + size_t ne = edges->data.function.arg_count; + + /* Undirected adjacency (dedup) + degrees. */ + char* adj = (n > 0) ? calloc((size_t)n * (size_t)n, 1) : NULL; + int* deg = (n > 0) ? calloc((size_t)n, sizeof(int)) : NULL; + for (size_t e = 0; e < ne; e++) { + const Expr* ed = edges->data.function.args[e]; + int a = graph_vertex_index(verts, ed->data.function.args[0]); + int b = graph_vertex_index(verts, ed->data.function.args[1]); + if (a < 0 || b < 0 || a == b) continue; + if (!adj[(size_t)a * n + b]) { + adj[(size_t)a * n + b] = adj[(size_t)b * n + a] = 1; + deg[a]++; deg[b]++; + } + } + + long M = 0, A = 0, S1 = 0, Sq = 0; + for (int i = 0; i < n; i++) + for (int k = i + 1; k < n; k++) + if (adj[(size_t)i * n + k]) { + long j = deg[i], l = deg[k]; + M++; A += j * l; S1 += j + l; Sq += j * j + l * l; + } + free(adj); free(deg); + + long den = 2 * M * Sq - S1 * S1; + if (den == 0) return expr_new_symbol("Indeterminate"); /* regular / edgeless */ + return ratio(4 * M * A - S1 * S1, den); +} diff --git a/src/graph/graphdifference.c b/src/graph/graphdifference.c new file mode 100644 index 00000000..e028631a --- /dev/null +++ b/src/graph/graphdifference.c @@ -0,0 +1,66 @@ +/* graphdifference.c - GraphDifference[g1, g2]: the graph on g1's vertices whose + * edges are those of g1 that do not appear in g2, matched by structural + * identity. + * + * Vertices are exactly those of g1 (order preserved). An edge of g1 is kept iff + * no equal edge appears in g2, where equality respects edge kind and is + * symmetric in the endpoints for undirected edges. Removing edges never + * invalidates the vertex set, so the result is a valid canonical Graph. + * O(E1*E2) from the linear edge-membership scan (small-graph scale). The third + * of the graph set-operation family (with GraphUnion and GraphIntersection). + * + * Memory (SPEC section 4): returns a freshly-built Graph; frees res. NULL unless + * both arguments are valid graphs. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +/* Structural edge equality, symmetric for undirected edges. */ +static int same_edge(const Expr* a, const Expr* b) { + const char* ka = graph_edge_kind(a); + const char* kb = graph_edge_kind(b); + if (!ka || ka != kb) return 0; + const Expr* a0 = a->data.function.args[0]; + const Expr* a1 = a->data.function.args[1]; + const Expr* b0 = b->data.function.args[0]; + const Expr* b1 = b->data.function.args[1]; + if (expr_eq(a0, b0) && expr_eq(a1, b1)) return 1; + if (ka == SYM_UndirectedEdge && expr_eq(a0, b1) && expr_eq(a1, b0)) return 1; + return 0; +} + +Expr* builtin_graph_difference(Expr* res) { + if (res->data.function.arg_count != 2) return NULL; + const Expr* g1 = res->data.function.args[0]; + const Expr* g2 = res->data.function.args[1]; + if (!graph_is_valid(g1) || !graph_is_valid(g2)) return NULL; + + const Expr* v1 = g1->data.function.args[0]; + const Expr* e1 = g1->data.function.args[1]; + const Expr* e2 = g2->data.function.args[1]; + size_t n1 = v1->data.function.arg_count; + size_t m1 = e1->data.function.arg_count, m2 = e2->data.function.arg_count; + + Expr** vc = malloc((n1 > 0 ? n1 : 1) * sizeof(Expr*)); + for (size_t i = 0; i < n1; i++) vc[i] = expr_copy(v1->data.function.args[i]); + + /* Edges of g1 with no equal edge in g2. */ + Expr** ec = malloc((m1 > 0 ? m1 : 1) * sizeof(Expr*)); + size_t me = 0; + for (size_t i = 0; i < m1; i++) { + const Expr* e = e1->data.function.args[i]; + int in2 = 0; + for (size_t j = 0; j < m2; j++) + if (same_edge(e, e2->data.function.args[j])) { in2 = 1; break; } + if (!in2) ec[me++] = expr_copy((Expr*)e); + } + + Expr* vlist = expr_new_function(expr_new_symbol(SYM_List), vc, n1); + Expr* elist = expr_new_function(expr_new_symbol(SYM_List), ec, me); + free(vc); free(ec); + Expr* gargs[2] = { vlist, elist }; + return expr_new_function(expr_new_symbol(SYM_Graph), gargs, 2); +} diff --git a/src/graph/graphdisjointunion.c b/src/graph/graphdisjointunion.c new file mode 100644 index 00000000..322c4199 --- /dev/null +++ b/src/graph/graphdisjointunion.c @@ -0,0 +1,59 @@ +/* graphdisjointunion.c - GraphDisjointUnion[g1, g2]: the disjoint union of g1 and + * g2. The vertices are relabelled to 1..n1+n2 (g1's block first, g2's block + * offset by n1) and the edges are those of g1 and g2 (relabelled, kinds + * preserved) with NO edges between the two blocks -- so the result has exactly + * the two graphs as its components. + * + * Relabelling to consecutive integers makes the union genuinely disjoint even + * when the graphs share vertex names. n1+n2 vertices, m1+m2 edges. O(V+E). It is + * GraphJoin without the n1*n2 cross edges. + * + * Memory (SPEC section 4): returns a freshly-built Graph; frees res. NULL unless + * both arguments are valid graphs. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +/* Copy an edge with endpoints relabelled to 1-based integers by their index in + * the source vertex list, offset by `base`. Preserves edge kind. */ +static Expr* relabel_edge(const Expr* verts, const Expr* e, int base) { + const char* kind = graph_edge_kind(e); + int ia = graph_vertex_index(verts, e->data.function.args[0]); + int ib = graph_vertex_index(verts, e->data.function.args[1]); + Expr* args[2] = { expr_new_integer(base + ia + 1), expr_new_integer(base + ib + 1) }; + return expr_new_function(expr_new_symbol(kind), args, 2); +} + +Expr* builtin_graph_disjoint_union(Expr* res) { + if (res->data.function.arg_count != 2) return NULL; + const Expr* g1 = res->data.function.args[0]; + const Expr* g2 = res->data.function.args[1]; + if (!graph_is_valid(g1) || !graph_is_valid(g2)) return NULL; + + const Expr* v1 = g1->data.function.args[0]; + const Expr* e1 = g1->data.function.args[1]; + const Expr* v2 = g2->data.function.args[0]; + const Expr* e2 = g2->data.function.args[1]; + int n1 = (int)v1->data.function.arg_count; + int n2 = (int)v2->data.function.arg_count; + size_t m1 = e1->data.function.arg_count, m2 = e2->data.function.arg_count; + + int N = n1 + n2; + Expr** vc = malloc((N > 0 ? N : 1) * sizeof(Expr*)); + for (int i = 0; i < N; i++) vc[i] = expr_new_integer(i + 1); + + size_t total = m1 + m2; + Expr** ec = (total > 0) ? malloc(total * sizeof(Expr*)) : NULL; + size_t me = 0; + for (size_t i = 0; i < m1; i++) ec[me++] = relabel_edge(v1, e1->data.function.args[i], 0); + for (size_t i = 0; i < m2; i++) ec[me++] = relabel_edge(v2, e2->data.function.args[i], n1); + + Expr* vlist = expr_new_function(expr_new_symbol(SYM_List), vc, (size_t)N); + Expr* elist = expr_new_function(expr_new_symbol(SYM_List), ec, me); + free(vc); free(ec); + Expr* gargs[2] = { vlist, elist }; + return expr_new_function(expr_new_symbol(SYM_Graph), gargs, 2); +} diff --git a/src/graph/graphintersection.c b/src/graph/graphintersection.c new file mode 100644 index 00000000..1e3f9bd1 --- /dev/null +++ b/src/graph/graphintersection.c @@ -0,0 +1,71 @@ +/* graphintersection.c - GraphIntersection[g1, g2]: the graph whose vertices are + * common to both graphs and whose edges are present in both, matched by + * structural identity. + * + * Vertices are those of g1 that also appear in g2 (g1 order preserved). An edge + * of g1 is kept iff an equal edge appears in g2, where equality respects edge + * kind and is symmetric in the endpoints for undirected edges. A kept edge's + * endpoints are necessarily common vertices, so the result is a valid canonical + * Graph. O(V1*V2 + E1*E2) from the linear membership scans (small-graph scale). + * + * Memory (SPEC section 4): returns a freshly-built Graph; frees res. NULL unless + * both arguments are valid graphs. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +/* Structural edge equality, symmetric for undirected edges. */ +static int same_edge(const Expr* a, const Expr* b) { + const char* ka = graph_edge_kind(a); + const char* kb = graph_edge_kind(b); + if (!ka || ka != kb) return 0; + const Expr* a0 = a->data.function.args[0]; + const Expr* a1 = a->data.function.args[1]; + const Expr* b0 = b->data.function.args[0]; + const Expr* b1 = b->data.function.args[1]; + if (expr_eq(a0, b0) && expr_eq(a1, b1)) return 1; + if (ka == SYM_UndirectedEdge && expr_eq(a0, b1) && expr_eq(a1, b0)) return 1; + return 0; +} + +Expr* builtin_graph_intersection(Expr* res) { + if (res->data.function.arg_count != 2) return NULL; + const Expr* g1 = res->data.function.args[0]; + const Expr* g2 = res->data.function.args[1]; + if (!graph_is_valid(g1) || !graph_is_valid(g2)) return NULL; + + const Expr* v1 = g1->data.function.args[0]; + const Expr* e1 = g1->data.function.args[1]; + const Expr* v2 = g2->data.function.args[0]; + const Expr* e2 = g2->data.function.args[1]; + size_t n1 = v1->data.function.arg_count; + size_t m1 = e1->data.function.arg_count, m2 = e2->data.function.arg_count; + + /* Vertices of g1 that also occur in g2 (g1 order). */ + Expr** vc = malloc((n1 > 0 ? n1 : 1) * sizeof(Expr*)); + size_t nv = 0; + for (size_t i = 0; i < n1; i++) { + const Expr* v = v1->data.function.args[i]; + if (graph_vertex_index(v2, v) >= 0) vc[nv++] = expr_copy((Expr*)v); + } + + /* Edges of g1 that also occur in g2. */ + Expr** ec = malloc((m1 > 0 ? m1 : 1) * sizeof(Expr*)); + size_t me = 0; + for (size_t i = 0; i < m1; i++) { + const Expr* e = e1->data.function.args[i]; + int both = 0; + for (size_t j = 0; j < m2; j++) + if (same_edge(e, e2->data.function.args[j])) { both = 1; break; } + if (both) ec[me++] = expr_copy((Expr*)e); + } + + Expr* vlist = expr_new_function(expr_new_symbol(SYM_List), vc, nv); + Expr* elist = expr_new_function(expr_new_symbol(SYM_List), ec, me); + free(vc); free(ec); + Expr* gargs[2] = { vlist, elist }; + return expr_new_function(expr_new_symbol(SYM_Graph), gargs, 2); +} diff --git a/src/graph/graphjoin.c b/src/graph/graphjoin.c new file mode 100644 index 00000000..fb49626a --- /dev/null +++ b/src/graph/graphjoin.c @@ -0,0 +1,66 @@ +/* graphjoin.c - GraphJoin[g1, g2]: the graph join. The vertices are the disjoint + * union of g1's and g2's, relabelled 1..n1+n2 (g1's block first), the edges are + * those of g1 and g2 (relabelled) together with an undirected edge joining every + * g1 vertex to every g2 vertex. + * + * Relabelling to consecutive integers gives a genuine disjoint union even when + * the two graphs share vertex names. Within each block the original edge kind is + * preserved; the n1*n2 cross edges are undirected. The result has + * m1 + m2 + n1*n2 edges and is a canonical Graph. O((V+E) + n1*n2). + * + * K1 join K1 is a single edge; P2 join K1 is a triangle; K_m join K_n is + * K_{m+n}. Memory (SPEC section 4): returns a freshly-built Graph; frees res. + * NULL unless both arguments are valid graphs. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +/* Copy an edge with endpoints relabelled to 1-based integers by their index in + * the source vertex list, offset by `base`. Preserves edge kind. */ +static Expr* relabel_edge(const Expr* verts, const Expr* e, int base) { + const char* kind = graph_edge_kind(e); + int ia = graph_vertex_index(verts, e->data.function.args[0]); + int ib = graph_vertex_index(verts, e->data.function.args[1]); + Expr* args[2] = { expr_new_integer(base + ia + 1), expr_new_integer(base + ib + 1) }; + return expr_new_function(expr_new_symbol(kind), args, 2); +} + +Expr* builtin_graph_join(Expr* res) { + if (res->data.function.arg_count != 2) return NULL; + const Expr* g1 = res->data.function.args[0]; + const Expr* g2 = res->data.function.args[1]; + if (!graph_is_valid(g1) || !graph_is_valid(g2)) return NULL; + + const Expr* v1 = g1->data.function.args[0]; + const Expr* e1 = g1->data.function.args[1]; + const Expr* v2 = g2->data.function.args[0]; + const Expr* e2 = g2->data.function.args[1]; + int n1 = (int)v1->data.function.arg_count; + int n2 = (int)v2->data.function.arg_count; + size_t m1 = e1->data.function.arg_count, m2 = e2->data.function.arg_count; + + /* Vertices 1 .. n1+n2. */ + int N = n1 + n2; + Expr** vc = malloc((N > 0 ? N : 1) * sizeof(Expr*)); + for (int i = 0; i < N; i++) vc[i] = expr_new_integer(i + 1); + + size_t total = m1 + m2 + (size_t)n1 * (size_t)n2; + Expr** ec = malloc((total > 0 ? total : 1) * sizeof(Expr*)); + size_t me = 0; + for (size_t i = 0; i < m1; i++) ec[me++] = relabel_edge(v1, e1->data.function.args[i], 0); + for (size_t i = 0; i < m2; i++) ec[me++] = relabel_edge(v2, e2->data.function.args[i], n1); + for (int i = 0; i < n1; i++) + for (int j = 0; j < n2; j++) { + Expr* args[2] = { expr_new_integer(i + 1), expr_new_integer(n1 + j + 1) }; + ec[me++] = expr_new_function(expr_new_symbol(SYM_UndirectedEdge), args, 2); + } + + Expr* vlist = expr_new_function(expr_new_symbol(SYM_List), vc, (size_t)N); + Expr* elist = expr_new_function(expr_new_symbol(SYM_List), ec, me); + free(vc); free(ec); + Expr* gargs[2] = { vlist, elist }; + return expr_new_function(expr_new_symbol(SYM_Graph), gargs, 2); +} diff --git a/src/graph/graphplot.c b/src/graph/graphplot.c new file mode 100644 index 00000000..d15a1b21 --- /dev/null +++ b/src/graph/graphplot.c @@ -0,0 +1,178 @@ +/* graphplot.c - GraphPlot[g] and the shared graph renderer. + * + * graph_render() turns a validated graph into a Graphics[...] tree of the same + * primitives the plotting engine already draws (Line, Disk, Text), preceded by + * RGBColor directives for per-element styling. The notebook Plotly serializer + * and the Raylib renderer both consume this with no special-casing, and the + * text placeholder is used when USE_GRAPHICS=0. + * + * Vertex positions come from src/graph/layout.c, selected by the GraphLayout + * option (circular by default). GraphPlot[g, opts] parses GraphLayout, + * VertexStyle, EdgeStyle, VertexLabels and VertexSize; HighlightGraph reuses + * graph_render() through the highlight masks in GraphStyle. + * + * Memory (SPEC section 4): builtins return a freshly-allocated Graphics tree; + * the evaluator frees res. graph_render is read-only over its arguments. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include "eval.h" +#include +#include + +#define NODE_RADIUS 0.08 + +/* Default palette (RGB in 0..1). */ +static const double DEF_VERTEX[3] = { 0.20, 0.42, 0.82 }; /* blue */ +static const double DEF_EDGE[3] = { 0.52, 0.56, 0.66 }; /* slate */ +static const double ACCENT[3] = { 0.95, 0.45, 0.10 }; /* highlight orange */ +static const double DIM[3] = { 0.80, 0.83, 0.88 }; /* dimmed grey */ + +static Expr* point2(double x, double y) { + Expr* xy[2] = { expr_new_real(x), expr_new_real(y) }; + return expr_new_function(expr_new_symbol(SYM_List), xy, 2); +} + +static Expr* rgb(const double c[3]) { + Expr* a[3] = { expr_new_real(c[0]), expr_new_real(c[1]), expr_new_real(c[2]) }; + return expr_new_function(expr_new_symbol(SYM_RGBColor), a, 3); +} + +/* A color directive for element `idx`, honoring highlight masks first, then the + * explicit style color, then the default. `mask` may be NULL. */ +static Expr* color_for(const char* mask, int idx, const Expr* style_color, + const double def[3]) { + if (mask) return rgb(mask[idx] ? ACCENT : DIM); + if (style_color) return expr_copy((Expr*)style_color); + return rgb(def); +} + +Expr* graph_render(const Expr* g, const GraphStyle* st) { + if (!graph_is_valid(g)) return NULL; + GraphStyle defaults = {0}; + defaults.show_labels = 1; + if (!st) st = &defaults; + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + int n = (int)verts->data.function.arg_count; + size_t ne = edges->data.function.arg_count; + double radius = (st->vertex_size > 0.0) ? st->vertex_size : NODE_RADIUS; + int labels = st->show_labels; + + double* x = (n > 0) ? calloc((size_t)n, sizeof(double)) : NULL; + double* y = (n > 0) ? calloc((size_t)n, sizeof(double)) : NULL; + if (n > 0 && (!x || !y)) { free(x); free(y); return NULL; } + graph_compute_layout(g, st->layout, x, y); + + /* Worst case: (color + line) per edge, (color + disk) per vertex, and one + * label per vertex. */ + size_t cap = ne * 2 + (size_t)n * 3 + 1; + Expr** prims = (cap > 0) ? calloc(cap, sizeof(Expr*)) : NULL; + size_t p = 0; + + /* Edges first so vertices draw on top. */ + for (size_t k = 0; k < ne; k++) { + const Expr* e = edges->data.function.args[k]; + int iu = graph_vertex_index(verts, e->data.function.args[0]); + int iv = graph_vertex_index(verts, e->data.function.args[1]); + if (iu < 0 || iv < 0) continue; + prims[p++] = color_for(st->hi_edge, (int)k, st->edge_color, DEF_EDGE); + Expr* pts[2] = { point2(x[iu], y[iu]), point2(x[iv], y[iv]) }; + Expr* seg = expr_new_function(expr_new_symbol(SYM_List), pts, 2); + Expr* la[1] = { seg }; + prims[p++] = expr_new_function(expr_new_symbol(SYM_Line), la, 1); + } + for (int i = 0; i < n; i++) { + prims[p++] = color_for(st->hi_vert, i, st->vertex_color, DEF_VERTEX); + Expr* da[2] = { point2(x[i], y[i]), expr_new_real(radius) }; + prims[p++] = expr_new_function(expr_new_symbol(SYM_Disk), da, 2); + } + if (labels) { + for (int i = 0; i < n; i++) { + Expr* ta[2] = { expr_copy((Expr*)verts->data.function.args[i]), point2(x[i], y[i]) }; + prims[p++] = expr_new_function(expr_new_symbol(SYM_Text), ta, 2); + } + } + + free(x); free(y); + Expr* prim_list = expr_new_function(expr_new_symbol(SYM_List), prims, p); + free(prims); + Expr* ga[1] = { prim_list }; + return expr_new_function(expr_new_symbol(SYM_Graphics), ga, 1); +} + +Expr* graph_default_graphics(const Expr* g) { + return graph_render(g, NULL); +} + +/* ---- option parsing ------------------------------------------------------- */ + +/* Resolve a color option value to an owned RGBColor[...] (converting GrayLevel + * and named colors, which evaluate to RGBColor). Returns NULL if not a color. */ +static Expr* resolve_color(const Expr* v) { + Expr* ev = evaluate(expr_copy((Expr*)v)); + if (!ev) return NULL; + if (ev->type == EXPR_FUNCTION && ev->data.function.head + && ev->data.function.head->type == EXPR_SYMBOL) { + const char* h = ev->data.function.head->data.symbol; + if (h == SYM_RGBColor && ev->data.function.arg_count >= 3) return ev; + if (h == SYM_GrayLevel && ev->data.function.arg_count >= 1) { + Expr* lv = ev->data.function.args[0]; + Expr* a[3] = { expr_copy(lv), expr_copy(lv), expr_copy(lv) }; + expr_free(ev); + return expr_new_function(expr_new_symbol(SYM_RGBColor), a, 3); + } + } + expr_free(ev); + return NULL; +} + +/* GraphPlot[g] / GraphPlot[g, opts...] */ +Expr* builtin_graph_plot(Expr* res) { + if (res->data.function.arg_count < 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + + GraphStyle st = {0}; + st.show_labels = 1; + Expr* vcol = NULL; Expr* ecol = NULL; /* owned; freed before return */ + + for (size_t i = 1; i < res->data.function.arg_count; i++) { + const Expr* opt = res->data.function.args[i]; + if (opt->type != EXPR_FUNCTION || opt->data.function.arg_count != 2 + || !opt->data.function.head + || opt->data.function.head->type != EXPR_SYMBOL + || !(opt->data.function.head->data.symbol == SYM_Rule + || opt->data.function.head->data.symbol == SYM_RuleDelayed)) + continue; + const char* name = (opt->data.function.args[0]->type == EXPR_SYMBOL) + ? opt->data.function.args[0]->data.symbol : NULL; + const Expr* rhs = opt->data.function.args[1]; + if (name == SYM_GraphLayout) { + if (rhs->type == EXPR_STRING) st.layout = rhs->data.string; + } else if (name == SYM_VertexStyle) { + if (vcol) expr_free(vcol); + vcol = resolve_color(rhs); st.vertex_color = vcol; + } else if (name == SYM_EdgeStyle) { + if (ecol) expr_free(ecol); + ecol = resolve_color(rhs); st.edge_color = ecol; + } else if (name == SYM_VertexSize) { + Expr* ev = evaluate(expr_copy((Expr*)rhs)); + if (ev && ev->type == EXPR_REAL) st.vertex_size = ev->data.real; + else if (ev && ev->type == EXPR_INTEGER) st.vertex_size = (double)ev->data.integer; + if (ev) expr_free(ev); + } else if (name == SYM_VertexLabels) { + if (rhs->type == EXPR_SYMBOL + && (rhs->data.symbol == SYM_None || rhs->data.symbol == SYM_False)) + st.show_labels = 0; + } + } + + Expr* out = graph_render(g, &st); + if (vcol) expr_free(vcol); + if (ecol) expr_free(ecol); + return out; +} diff --git a/src/graph/graphpower.c b/src/graph/graphpower.c new file mode 100644 index 00000000..9a72ff51 --- /dev/null +++ b/src/graph/graphpower.c @@ -0,0 +1,83 @@ +/* graphpower.c - GraphPower[g, k]: the k-th power of a graph. Two vertices are + * joined in the result iff they are connected by a path of length 1..k in g + * (a vertex is never joined to itself -- no self-loops). + * + * undirected g -> undirected power: {i,j} joined iff dist(i,j) <= k. + * directed g -> directed power: i->j iff the directed dist(i,j) <= k. + * + * Reachability comes from a depth-limited BFS per source over GraphAdj.out[], + * which already encodes direction (an undirected edge sits in both endpoints' + * out[]), so one traversal handles both cases. Cost O(V*(V+E)); the result is + * the canonical Graph value, ready to render or query. + * + * k must be a positive integer literal; otherwise the call is left unevaluated + * (NULL) so GraphPower[g, k] stays symbolic for non-numeric k. + * + * Memory (SPEC section 4): returns a freshly-built Graph tree; frees res. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +Expr* builtin_graph_power(Expr* res) { + if (res->data.function.arg_count != 2) return NULL; + const Expr* g = res->data.function.args[0]; + const Expr* ke = res->data.function.args[1]; + if (!graph_is_valid(g)) return NULL; + if (ke->type != EXPR_INTEGER || ke->data.integer < 1) return NULL; + long k = (long)ke->data.integer; + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + int n = (int)verts->data.function.arg_count; + size_t ne = edges->data.function.arg_count; + + int directed = (ne > 0); + for (size_t i = 0; i < ne; i++) + if (graph_edge_kind(edges->data.function.args[i]) != SYM_DirectedEdge) { directed = 0; break; } + + GraphAdj* a = graph_build_adj(g); + if (!a) return NULL; + + /* n(n-1) is the dense upper bound (undirected halves it). */ + size_t cap = (size_t)n * (n > 0 ? (size_t)(n - 1) : 0); + if (!directed) cap /= 2; + Expr** pedges = (cap > 0) ? calloc(cap, sizeof(Expr*)) : NULL; + size_t m = 0; + const char* ekind = directed ? SYM_DirectedEdge : SYM_UndirectedEdge; + + int* dist = (n > 0) ? malloc((size_t)n * sizeof(int)) : NULL; + int* queue = (n > 0) ? malloc((size_t)n * sizeof(int)) : NULL; + for (int s = 0; s < n; s++) { + for (int i = 0; i < n; i++) dist[i] = -1; + int qh = 0, qt = 0; + dist[s] = 0; queue[qt++] = s; + while (qh < qt) { + int v = queue[qh++]; + if (dist[v] >= (int)k) continue; /* depth-limited */ + for (int j = 0; j < a->outdeg[v]; j++) { + int u = a->out[v][j]; + if (dist[u] < 0) { dist[u] = dist[v] + 1; queue[qt++] = u; } + } + } + for (int t = 0; t < n; t++) { + if (t == s || dist[t] < 0) continue; /* unreachable / self */ + if (!directed && t < s) continue; /* undirected: emit once */ + Expr* args[2] = { expr_copy(verts->data.function.args[s]), + expr_copy(verts->data.function.args[t]) }; + pedges[m++] = expr_new_function(expr_new_symbol(ekind), args, 2); + } + } + free(dist); free(queue); + graph_adj_free(a); + + Expr** vcopy = (n > 0) ? calloc((size_t)n, sizeof(Expr*)) : NULL; + for (int i = 0; i < n; i++) vcopy[i] = expr_copy(verts->data.function.args[i]); + Expr* vlist = expr_new_function(expr_new_symbol(SYM_List), vcopy, (size_t)n); + Expr* elist = expr_new_function(expr_new_symbol(SYM_List), pedges, m); + free(vcopy); free(pedges); + Expr* gargs[2] = { vlist, elist }; + return expr_new_function(expr_new_symbol(SYM_Graph), gargs, 2); +} diff --git a/src/graph/graphproduct.c b/src/graph/graphproduct.c new file mode 100644 index 00000000..de26aedd --- /dev/null +++ b/src/graph/graphproduct.c @@ -0,0 +1,103 @@ +/* graphproduct.c - GraphProduct[g1, g2, type]: a product graph on the vertex set + * V1 x V2, with the four standard adjacency rules. Product vertices are the + * pairs {a, b} (a in g1, b in g2); the result is undirected (products are + * defined on the underlying undirected graphs). + * + * For product vertices (a1,b1) != (a2,b2), with A1/A2 the adjacencies of g1/g2: + * "Cartesian" joined iff (a1=a2 and b1~b2) or (b1=b2 and a1~a2) + * "Tensor" joined iff a1~a2 and b1~b2 (categorical/direct) + * "Strong" Cartesian OR Tensor + * "Lexicographic" joined iff a1~a2, or (a1=a2 and b1~b2) (composition) + * + * O((n1 n2)^2) over vertex pairs -- small-graph scale. Classic identities: + * P2 [] P2 = C4, K2 (Tensor) K2 = 2K2, K2 (Strong) K2 = K4, C4 [] K2 = the cube. + * + * Memory (SPEC section 4): returns a freshly-built Graph; frees res. NULL unless + * both arguments are valid graphs and the third is a known product-type string. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include +#include + +enum { PROD_CART, PROD_TENSOR, PROD_STRONG, PROD_LEX }; + +/* Undirected boolean adjacency for a validated graph (caller frees). */ +static char* build_adj(const Expr* g, int* np) { + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + int n = (int)verts->data.function.arg_count; + *np = n; + char* adj = (n > 0) ? calloc((size_t)n * (size_t)n, 1) : NULL; + for (size_t k = 0; k < edges->data.function.arg_count; k++) { + const Expr* e = edges->data.function.args[k]; + int ia = graph_vertex_index(verts, e->data.function.args[0]); + int ib = graph_vertex_index(verts, e->data.function.args[1]); + if (ia < 0 || ib < 0 || ia == ib) continue; + adj[(size_t)ia * n + ib] = adj[(size_t)ib * n + ia] = 1; + } + return adj; +} + +Expr* builtin_graph_product(Expr* res) { + if (res->data.function.arg_count != 3) return NULL; + const Expr* g1 = res->data.function.args[0]; + const Expr* g2 = res->data.function.args[1]; + const Expr* ty = res->data.function.args[2]; + if (!graph_is_valid(g1) || !graph_is_valid(g2)) return NULL; + if (ty->type != EXPR_STRING) return NULL; + int kind; + if (strcmp(ty->data.string, "Cartesian") == 0) kind = PROD_CART; + else if (strcmp(ty->data.string, "Tensor") == 0) kind = PROD_TENSOR; + else if (strcmp(ty->data.string, "Strong") == 0) kind = PROD_STRONG; + else if (strcmp(ty->data.string, "Lexicographic") == 0) kind = PROD_LEX; + else return NULL; + + const Expr* v1 = g1->data.function.args[0]; + const Expr* v2 = g2->data.function.args[0]; + int n1, n2; + char* a1 = build_adj(g1, &n1); + char* a2 = build_adj(g2, &n2); + int N = n1 * n2; + + /* Product vertices {a, b} at index i*n2 + j. */ + Expr** vc = malloc((N > 0 ? N : 1) * sizeof(Expr*)); + for (int i = 0; i < n1; i++) + for (int j = 0; j < n2; j++) { + Expr* pair[2] = { expr_copy(v1->data.function.args[i]), + expr_copy(v2->data.function.args[j]) }; + vc[i * n2 + j] = expr_new_function(expr_new_symbol(SYM_List), pair, 2); + } + + /* Edges over unordered product-vertex pairs. */ + size_t cap = (size_t)N * (N > 0 ? (size_t)(N - 1) : 0) / 2; + Expr** ec = (cap > 0) ? malloc(cap * sizeof(Expr*)) : NULL; + size_t me = 0; + for (int p = 0; p < N; p++) { + int i1 = p / n2, j1 = p % n2; + for (int q = p + 1; q < N; q++) { + int i2 = q / n2, j2 = q % n2; + int ea = (i1 != i2) && a1[(size_t)i1 * n1 + i2]; /* a1 ~ a2 */ + int eb = (j1 != j2) && a2[(size_t)j1 * n2 + j2]; /* b1 ~ b2 */ + int adj; + switch (kind) { + case PROD_CART: adj = (i1 == i2 && eb) || (j1 == j2 && ea); break; + case PROD_TENSOR: adj = ea && eb; break; + case PROD_STRONG: adj = ((i1 == i2 && eb) || (j1 == j2 && ea)) || (ea && eb); break; + default: adj = ea || (i1 == i2 && eb); break; /* Lexicographic */ + } + if (!adj) continue; + Expr* args[2] = { expr_copy(vc[p]), expr_copy(vc[q]) }; + ec[me++] = expr_new_function(expr_new_symbol(SYM_UndirectedEdge), args, 2); + } + } + free(a1); free(a2); + + Expr* vlist = expr_new_function(expr_new_symbol(SYM_List), vc, (size_t)N); + Expr* elist = expr_new_function(expr_new_symbol(SYM_List), ec, me); + free(vc); free(ec); + Expr* gargs[2] = { vlist, elist }; + return expr_new_function(expr_new_symbol(SYM_Graph), gargs, 2); +} diff --git a/src/graph/graphq.c b/src/graph/graphq.c new file mode 100644 index 00000000..0ce4a83c --- /dev/null +++ b/src/graph/graphq.c @@ -0,0 +1,19 @@ +/* graphq.c - GraphQ[g]: is g a valid graph? + * + * A thin wrapper over graph_is_valid (graph_util.c): returns the symbol True + * when the (already-evaluated) argument is a canonical, valid graph, and False + * otherwise. Non-unary calls are left unevaluated (NULL). + * + * Memory (SPEC section 4): returns a freshly-allocated symbol; the evaluator + * frees `res`. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" + +Expr* builtin_graph_q(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* arg = res->data.function.args[0]; + return expr_new_symbol(graph_is_valid(arg) ? SYM_True : SYM_False); +} diff --git a/src/graph/graphreverse.c b/src/graph/graphreverse.c new file mode 100644 index 00000000..11ff8494 --- /dev/null +++ b/src/graph/graphreverse.c @@ -0,0 +1,51 @@ +/* graphreverse.c - ReverseGraph[g]: g with the direction of every edge reversed. + * A DirectedEdge[a, b] becomes DirectedEdge[b, a]; an UndirectedEdge is left + * unchanged (it has no orientation). Vertices are preserved. + * + * The reverse (transpose) graph swaps successors and predecessors, so it turns + * out-degree into in-degree, reachability into reverse reachability, and is an + * involution: ReverseGraph[ReverseGraph[g]] === g. For an undirected graph it is + * the identity. O(V + E), returns a canonical Graph. + * + * Memory (SPEC section 4): returns a freshly-built Graph; frees res. NULL on a + * non-graph argument. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +Expr* builtin_graph_reverse(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + size_t n = verts->data.function.arg_count; + size_t m = edges->data.function.arg_count; + + Expr** vc = malloc((n > 0 ? n : 1) * sizeof(Expr*)); + for (size_t i = 0; i < n; i++) vc[i] = expr_copy(verts->data.function.args[i]); + + Expr** ec = malloc((m > 0 ? m : 1) * sizeof(Expr*)); + for (size_t i = 0; i < m; i++) { + const Expr* e = edges->data.function.args[i]; + const char* kind = graph_edge_kind(e); + const Expr* a = e->data.function.args[0]; + const Expr* b = e->data.function.args[1]; + if (kind == SYM_DirectedEdge) { /* swap endpoints */ + Expr* args[2] = { expr_copy((Expr*)b), expr_copy((Expr*)a) }; + ec[i] = expr_new_function(expr_new_symbol(SYM_DirectedEdge), args, 2); + } else { /* undirected: unchanged */ + ec[i] = expr_copy((Expr*)e); + } + } + + Expr* vlist = expr_new_function(expr_new_symbol(SYM_List), vc, n); + Expr* elist = expr_new_function(expr_new_symbol(SYM_List), ec, m); + free(vc); free(ec); + Expr* gargs[2] = { vlist, elist }; + return expr_new_function(expr_new_symbol(SYM_Graph), gargs, 2); +} diff --git a/src/graph/graphunion.c b/src/graph/graphunion.c new file mode 100644 index 00000000..aa097338 --- /dev/null +++ b/src/graph/graphunion.c @@ -0,0 +1,81 @@ +/* graphunion.c - GraphUnion[g1, g2]: the graph whose vertex set is the union of + * the two vertex sets and whose edge set is the union of the two edge sets, + * matched by vertex identity (structural equality). Duplicate vertices and + * edges are collapsed. + * + * Vertices from g1 keep their order; new vertices from g2 are appended. Edges + * likewise, with an edge from g2 added only if no equal edge is already present + * -- where "equal" respects edge kind and, for undirected edges, is symmetric in + * the endpoints. The result is returned as a canonical Graph[List, List]. + * O((V1+V2)^2 + (E1+E2)^2) from the linear membership scans (small-graph scale). + * + * Memory (SPEC section 4): returns a freshly-built Graph; frees res. NULL unless + * both arguments are valid graphs. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +/* Structural edge equality, symmetric for undirected edges. */ +static int same_edge(const Expr* a, const Expr* b) { + const char* ka = graph_edge_kind(a); + const char* kb = graph_edge_kind(b); + if (!ka || ka != kb) return 0; + const Expr* a0 = a->data.function.args[0]; + const Expr* a1 = a->data.function.args[1]; + const Expr* b0 = b->data.function.args[0]; + const Expr* b1 = b->data.function.args[1]; + if (expr_eq(a0, b0) && expr_eq(a1, b1)) return 1; + if (ka == SYM_UndirectedEdge && expr_eq(a0, b1) && expr_eq(a1, b0)) return 1; + return 0; +} + +Expr* builtin_graph_union(Expr* res) { + if (res->data.function.arg_count != 2) return NULL; + const Expr* g1 = res->data.function.args[0]; + const Expr* g2 = res->data.function.args[1]; + if (!graph_is_valid(g1) || !graph_is_valid(g2)) return NULL; + + const Expr* v1 = g1->data.function.args[0]; + const Expr* e1 = g1->data.function.args[1]; + const Expr* v2 = g2->data.function.args[0]; + const Expr* e2 = g2->data.function.args[1]; + size_t n1 = v1->data.function.arg_count, n2 = v2->data.function.arg_count; + size_t m1 = e1->data.function.arg_count, m2 = e2->data.function.arg_count; + + /* Union of vertices (g1 order, then new ones from g2). */ + const Expr** vp = malloc((n1 + n2) * sizeof(Expr*)); + size_t nv = 0; + for (size_t i = 0; i < n1; i++) vp[nv++] = v1->data.function.args[i]; + for (size_t i = 0; i < n2; i++) { + const Expr* v = v2->data.function.args[i]; + int seen = 0; + for (size_t j = 0; j < nv; j++) if (expr_eq(vp[j], v)) { seen = 1; break; } + if (!seen) vp[nv++] = v; + } + + /* Union of edges. */ + const Expr** ep = malloc(((m1 + m2) > 0 ? (m1 + m2) : 1) * sizeof(Expr*)); + size_t me = 0; + for (size_t i = 0; i < m1; i++) ep[me++] = e1->data.function.args[i]; + for (size_t i = 0; i < m2; i++) { + const Expr* e = e2->data.function.args[i]; + int seen = 0; + for (size_t j = 0; j < me; j++) if (same_edge(ep[j], e)) { seen = 1; break; } + if (!seen) ep[me++] = e; + } + + Expr** vc = malloc((nv > 0 ? nv : 1) * sizeof(Expr*)); + for (size_t i = 0; i < nv; i++) vc[i] = expr_copy((Expr*)vp[i]); + Expr** ec = malloc((me > 0 ? me : 1) * sizeof(Expr*)); + for (size_t i = 0; i < me; i++) ec[i] = expr_copy((Expr*)ep[i]); + free(vp); free(ep); + + Expr* vlist = expr_new_function(expr_new_symbol(SYM_List), vc, nv); + Expr* elist = expr_new_function(expr_new_symbol(SYM_List), ec, me); + free(vc); free(ec); + Expr* gargs[2] = { vlist, elist }; + return expr_new_function(expr_new_symbol(SYM_Graph), gargs, 2); +} diff --git a/src/graph/hamiltonianq.c b/src/graph/hamiltonianq.c new file mode 100644 index 00000000..91a1ccd7 --- /dev/null +++ b/src/graph/hamiltonianq.c @@ -0,0 +1,59 @@ +/* hamiltonianq.c - HamiltonianGraphQ[g]: True iff g has a Hamiltonian cycle (a + * closed walk visiting every vertex exactly once). The predicate companion to + * FindHamiltonianCycle, and the Hamiltonian counterpart of EulerianGraphQ. + * + * Depth-first backtracking over GraphAdj.out[] with visited-set pruning, rooted + * at vertex 0 (WLOG -- a Hamiltonian cycle passes through every vertex). At full + * depth the walk must close back to the start. Cheap necessary-condition prunes + * short-circuit the trivially impossible: fewer than 3 vertices, or any vertex + * missing an out- or in-neighbour. out[] encodes edge direction, so directed and + * undirected graphs share one code path. O(V!) worst case, tiny in practice. + * + * Memory (SPEC section 4): returns True/False; frees res. NULL on a non-graph. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +static int ham_exists(const GraphAdj* a, int* path, char* visited, int depth, int start) { + int last = path[depth - 1]; + if (depth == a->n) { + for (int j = 0; j < a->outdeg[last]; j++) + if (a->out[last][j] == start) return 1; + return 0; + } + for (int j = 0; j < a->outdeg[last]; j++) { + int u = a->out[last][j]; + if (!visited[u]) { + visited[u] = 1; + path[depth] = u; + if (ham_exists(a, path, visited, depth + 1, start)) return 1; + visited[u] = 0; + } + } + return 0; +} + +Expr* builtin_hamiltonian_graph_q(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + GraphAdj* a = graph_build_adj(res->data.function.args[0]); + if (!a) return NULL; + int n = a->n; + + int impossible = (n < 3); + for (int i = 0; i < n && !impossible; i++) + if (a->outdeg[i] == 0 || a->indeg[i] == 0) impossible = 1; + + int yes = 0; + if (!impossible) { + int* path = malloc((size_t)n * sizeof(int)); + char* visited = calloc((size_t)n, 1); + path[0] = 0; visited[0] = 1; + yes = ham_exists(a, path, visited, 1, 0); + free(path); free(visited); + } + graph_adj_free(a); + return expr_new_symbol(yes ? SYM_True : SYM_False); +} diff --git a/src/graph/helmgraph.c b/src/graph/helmgraph.c new file mode 100644 index 00000000..bc32b2db --- /dev/null +++ b/src/graph/helmgraph.c @@ -0,0 +1,46 @@ +/* helmgraph.c - HelmGraph[n]: the helm graph, a wheel (a hub joined to every + * vertex of an n-cycle rim) with one pendant vertex attached to each rim vertex. + * + * Vertices: hub = 1, rim = 2..n+1 (an n-cycle), pendants = n+2..2n+1 (pendant r + * attached to rim vertex r). Edges: the rim cycle, the n hub-rim spokes, and the + * n pendants -- 2n+1 vertices and 3n edges. The hub has degree n, each rim vertex + * degree 4 (two rim + spoke + pendant), each pendant degree 1. O(n). + * + * n >= 3 must be an integer. Memory (SPEC section 4): returns a freshly-built + * Graph; frees res. NULL otherwise. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +Expr* builtin_helm_graph(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* ne = res->data.function.args[0]; + if (ne->type != EXPR_INTEGER) return NULL; + long n = (long)ne->data.integer; + if (n < 3) return NULL; + + long V = 2 * n + 1; + Expr** vc = malloc((size_t)V * sizeof(Expr*)); + for (long i = 0; i < V; i++) vc[i] = expr_new_integer(i + 1); + + Expr** ec = malloc((size_t)(3 * n) * sizeof(Expr*)); + size_t me = 0; + #define ADD(a,b) do { Expr* _e[2] = { expr_new_integer(a), expr_new_integer(b) }; \ + ec[me++] = expr_new_function(expr_new_symbol(SYM_UndirectedEdge), _e, 2); } while (0) + for (long r = 0; r < n; r++) { + long rim = 2 + r, rim1 = 2 + (r + 1) % n, pend = n + 2 + r; + ADD(rim, rim1); /* rim cycle */ + ADD(1, rim); /* hub spoke */ + ADD(rim, pend); /* pendant */ + } + #undef ADD + + Expr* vlist = expr_new_function(expr_new_symbol(SYM_List), vc, (size_t)V); + Expr* elist = expr_new_function(expr_new_symbol(SYM_List), ec, me); + free(vc); free(ec); + Expr* gargs[2] = { vlist, elist }; + return expr_new_function(expr_new_symbol(SYM_Graph), gargs, 2); +} diff --git a/src/graph/highlight.c b/src/graph/highlight.c new file mode 100644 index 00000000..77190b6b --- /dev/null +++ b/src/graph/highlight.c @@ -0,0 +1,128 @@ +/* highlight.c - HighlightGraph[g, parts]: draw g with selected vertices/edges + * emphasized. + * + * Returns a styled Graphics[...] (like GraphPlot) in which highlighted elements + * use the accent color and everything else is dimmed, so it auto-displays as a + * diagram. `parts` is a list whose elements may be: + * - a vertex -> that vertex is highlighted; + * - an edge (u<->v, u->v, Directed/UndirectedEdge[u,v]) -> that edge; + * - a list of vertices -> a path: its vertices AND the edges joining + * consecutive vertices are highlighted. + * Edge endpoints are matched unordered, so direction need not be specified. + * + * We deliberately return a Graphics rather than a Graph: the canonical + * Graph[List,List] form is locked to simple graphs (no annotations), so the + * highlight lives only in the rendered picture. Layout/styling options accepted + * by GraphPlot (GraphLayout, VertexStyle, EdgeStyle, VertexLabels, VertexSize) + * pass through unchanged. + * + * Memory (SPEC section 4): returns a fresh Graphics tree; the evaluator frees + * res. Read-only over the input graph. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +/* Index of the graph edge (in canonical order) whose endpoints equal {a,b} + * as an unordered pair, or -1. */ +static int find_edge(const Expr* edges, const Expr* verts, const Expr* a, + const Expr* b) { + (void)verts; + size_t ne = edges->data.function.arg_count; + for (size_t k = 0; k < ne; k++) { + const Expr* e = edges->data.function.args[k]; + if (e->type != EXPR_FUNCTION || e->data.function.arg_count != 2) continue; + const Expr* u = e->data.function.args[0]; + const Expr* v = e->data.function.args[1]; + if ((expr_eq((Expr*)u, (Expr*)a) && expr_eq((Expr*)v, (Expr*)b)) + || (expr_eq((Expr*)u, (Expr*)b) && expr_eq((Expr*)v, (Expr*)a))) + return (int)k; + } + return -1; +} + +/* True if `e` looks like a 2-endpoint edge spec (any of the four heads). */ +static int is_edge_spec(const Expr* e) { + if (e->type != EXPR_FUNCTION || e->data.function.arg_count != 2 + || !e->data.function.head || e->data.function.head->type != EXPR_SYMBOL) + return 0; + const char* h = e->data.function.head->data.symbol; + return h == SYM_DirectedEdge || h == SYM_UndirectedEdge + || h == SYM_Rule || h == SYM_TwoWayRule; +} + +static void mark_vertex(const Expr* verts, char* hv, const Expr* v) { + int idx = graph_vertex_index(verts, v); + if (idx >= 0) hv[idx] = 1; +} + +static void mark_edge(const Expr* edges, const Expr* verts, char* he, + const Expr* a, const Expr* b) { + int idx = find_edge(edges, verts, a, b); + if (idx >= 0) he[idx] = 1; +} + +Expr* builtin_highlight_graph(Expr* res) { + if (res->data.function.arg_count < 2) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + const Expr* parts = res->data.function.args[1]; + if (parts->type != EXPR_FUNCTION || !parts->data.function.head + || parts->data.function.head->type != EXPR_SYMBOL + || parts->data.function.head->data.symbol != SYM_List) + return NULL; + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + int n = (int)verts->data.function.arg_count; + size_t ne = edges->data.function.arg_count; + + char* hv = calloc((size_t)(n > 0 ? n : 1), 1); + char* he = calloc((ne > 0 ? ne : 1), 1); + if (!hv || !he) { free(hv); free(he); return NULL; } + + for (size_t i = 0; i < parts->data.function.arg_count; i++) { + const Expr* part = parts->data.function.args[i]; + if (is_edge_spec(part)) { + mark_edge(edges, verts, he, part->data.function.args[0], + part->data.function.args[1]); + } else if (part->type == EXPR_FUNCTION && part->data.function.head + && part->data.function.head->type == EXPR_SYMBOL + && part->data.function.head->data.symbol == SYM_List) { + /* A path: highlight its vertices and the joining edges. */ + size_t m = part->data.function.arg_count; + for (size_t j = 0; j < m; j++) + mark_vertex(verts, hv, part->data.function.args[j]); + for (size_t j = 1; j < m; j++) + mark_edge(edges, verts, he, part->data.function.args[j - 1], + part->data.function.args[j]); + } else { + mark_vertex(verts, hv, part); + } + } + + /* Reuse GraphPlot's option parsing for any trailing GraphLayout/style opts + * by carrying them into a GraphStyle here. */ + GraphStyle st = {0}; + st.show_labels = 1; + st.hi_vert = hv; + st.hi_edge = he; + for (size_t i = 2; i < res->data.function.arg_count; i++) { + const Expr* opt = res->data.function.args[i]; + if (opt->type == EXPR_FUNCTION && opt->data.function.arg_count == 2 + && opt->data.function.head + && opt->data.function.head->type == EXPR_SYMBOL + && opt->data.function.head->data.symbol == SYM_Rule + && opt->data.function.args[0]->type == EXPR_SYMBOL + && opt->data.function.args[0]->data.symbol == SYM_GraphLayout + && opt->data.function.args[1]->type == EXPR_STRING) { + st.layout = opt->data.function.args[1]->data.string; + } + } + + Expr* out = graph_render(g, &st); + free(hv); free(he); + return out; +} diff --git a/src/graph/icosahedralgraph.c b/src/graph/icosahedralgraph.c new file mode 100644 index 00000000..9719fca3 --- /dev/null +++ b/src/graph/icosahedralgraph.c @@ -0,0 +1,47 @@ +/* icosahedralgraph.c - IcosahedralGraph[]: the graph of the regular icosahedron + * (a Platonic solid). It is a pentagonal antiprism (two 5-cycle rings joined in + * antiprism fashion) capped by two apex vertices, one joined to each ring. + * + * 12 vertices: top apex = 1, top ring = 2..6, bottom ring = 7..11, bottom apex = + * 12. Edges (30): the two ring 5-cycles, the apex-to-ring spokes (5 each), and the + * antiprism links top_r ~ bottom_r and top_r ~ bottom_{r+1}. 5-regular, connected, + * non-bipartite, chromatic number 4, Hamiltonian. Takes no arguments. O(1). + * + * Memory (SPEC section 4): returns a freshly-built Graph; frees res. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +Expr* builtin_icosahedral_graph(Expr* res) { + if (res->data.function.arg_count != 0) return NULL; + + enum { V = 12 }; + Expr** vc = malloc(V * sizeof(Expr*)); + for (int i = 0; i < V; i++) vc[i] = expr_new_integer(i + 1); + + Expr** ec = malloc(30 * sizeof(Expr*)); + size_t me = 0; + #define ADD(a,b) do { Expr* _e[2] = { expr_new_integer(a), expr_new_integer(b) }; \ + ec[me++] = expr_new_function(expr_new_symbol(SYM_UndirectedEdge), _e, 2); } while (0) + const int TOP = 1, BOT = 12; + for (int r = 0; r < 5; r++) { + int t = 2 + r, t1 = 2 + (r + 1) % 5; /* top ring vertex + next */ + int b = 7 + r, b1 = 7 + (r + 1) % 5; /* bottom ring vertex + next */ + ADD(t, t1); /* top ring cycle */ + ADD(b, b1); /* bottom ring cycle */ + ADD(TOP, t); /* top apex spoke */ + ADD(BOT, b); /* bottom apex spoke */ + ADD(t, b); /* antiprism link */ + ADD(t, b1); /* offset antiprism link */ + } + #undef ADD + + Expr* vlist = expr_new_function(expr_new_symbol(SYM_List), vc, (size_t)V); + Expr* elist = expr_new_function(expr_new_symbol(SYM_List), ec, me); + free(vc); free(ec); + Expr* gargs[2] = { vlist, elist }; + return expr_new_function(expr_new_symbol(SYM_Graph), gargs, 2); +} diff --git a/src/graph/incidencelist.c b/src/graph/incidencelist.c new file mode 100644 index 00000000..8029bbd7 --- /dev/null +++ b/src/graph/incidencelist.c @@ -0,0 +1,38 @@ +/* incidencelist.c - IncidenceList[g, v]: the list of edges of g incident to + * vertex v (v is one of the endpoints), in the graph's edge order. For a directed + * graph this includes both the out-edges and in-edges at v. Complements + * AdjacencyList, which returns the neighbouring vertices rather than the edges. + * + * O(E) with a linear endpoint comparison; edge kinds are preserved. An edge + * appears once even if both endpoints equal v (impossible here -- no self-loops). + * A vertex not in g yields {}. + * + * Memory (SPEC section 4): returns a fresh List; frees res. NULL on a non-graph. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +Expr* builtin_incidence_list(Expr* res) { + if (res->data.function.arg_count != 2) return NULL; + const Expr* g = res->data.function.args[0]; + const Expr* v = res->data.function.args[1]; + if (!graph_is_valid(g)) return NULL; + + const Expr* edges = g->data.function.args[1]; + size_t m = edges->data.function.arg_count; + + Expr** items = (m > 0) ? malloc(m * sizeof(Expr*)) : NULL; + size_t cnt = 0; + for (size_t k = 0; k < m; k++) { + const Expr* e = edges->data.function.args[k]; + if (expr_eq(e->data.function.args[0], v) || expr_eq(e->data.function.args[1], v)) + items[cnt++] = expr_copy((Expr*)e); + } + + Expr* out = expr_new_function(expr_new_symbol(SYM_List), items, cnt); + free(items); + return out; +} diff --git a/src/graph/incmat.c b/src/graph/incmat.c new file mode 100644 index 00000000..9bbee628 --- /dev/null +++ b/src/graph/incmat.c @@ -0,0 +1,54 @@ +/* incmat.c - IncidenceMatrix[g]: |V| x |E| incidence matrix. + * + * Column j corresponds to edge j (canonical order), row i to vertex i. + * - UndirectedEdge{a,b}: entries (a,j) and (b,j) are 1. + * - DirectedEdge[a,b]: (a,j) = -1 (tail), (b,j) = 1 (head) [oriented]. + * + * Memory (SPEC section 4): returns a freshly-allocated matrix; frees res. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +Expr* builtin_incidence_matrix(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + size_t n = verts->data.function.arg_count; + size_t m = edges->data.function.arg_count; + + int* grid = (n > 0 && m > 0) ? calloc(n * m, sizeof(int)) : NULL; + if (n > 0 && m > 0 && !grid) return NULL; + + for (size_t j = 0; j < m; j++) { + const Expr* e = edges->data.function.args[j]; + const char* kind = graph_edge_kind(e); + int ia = graph_vertex_index(verts, e->data.function.args[0]); + int ib = graph_vertex_index(verts, e->data.function.args[1]); + if (kind == SYM_UndirectedEdge) { + grid[(size_t)ia * m + j] = 1; + grid[(size_t)ib * m + j] = 1; + } else { + grid[(size_t)ia * m + j] = -1; /* tail */ + grid[(size_t)ib * m + j] = 1; /* head */ + } + } + + Expr** rows = (n > 0) ? calloc(n, sizeof(Expr*)) : NULL; + for (size_t i = 0; i < n; i++) { + Expr** row = (m > 0) ? calloc(m, sizeof(Expr*)) : NULL; + for (size_t j = 0; j < m; j++) + row[j] = expr_new_integer(grid[i * m + j]); + rows[i] = expr_new_function(expr_new_symbol(SYM_List), row, m); + free(row); + } + Expr* mat = expr_new_function(expr_new_symbol(SYM_List), rows, n); + free(rows); + free(grid); + return mat; +} diff --git a/src/graph/indexgraph.c b/src/graph/indexgraph.c new file mode 100644 index 00000000..6e08bee3 --- /dev/null +++ b/src/graph/indexgraph.c @@ -0,0 +1,55 @@ +/* indexgraph.c - IndexGraph[g] / IndexGraph[g, k]: g with its vertices renamed to + * consecutive integers starting at 1 (or at k), in their current order, with + * every edge remapped accordingly and edge kinds preserved. + * + * Useful for normalising arbitrary vertex labels (symbols, strings, nested + * expressions) to a canonical integer indexing before matrix work or + * comparison. O(V + E) with the linear vertex-index lookup. Returns a canonical + * Graph. + * + * Memory (SPEC section 4): returns a freshly-built Graph; frees res. NULL on a + * non-graph or a non-integer start. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +Expr* builtin_index_graph(Expr* res) { + size_t argc = res->data.function.arg_count; + if (argc != 1 && argc != 2) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + + long start = 1; + if (argc == 2) { + const Expr* s = res->data.function.args[1]; + if (s->type != EXPR_INTEGER) return NULL; + start = (long)s->data.integer; + } + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + size_t n = verts->data.function.arg_count; + size_t m = edges->data.function.arg_count; + + Expr** vc = malloc((n > 0 ? n : 1) * sizeof(Expr*)); + for (size_t i = 0; i < n; i++) vc[i] = expr_new_integer(start + (long)i); + + Expr** ec = malloc((m > 0 ? m : 1) * sizeof(Expr*)); + for (size_t i = 0; i < m; i++) { + const Expr* e = edges->data.function.args[i]; + const char* kind = graph_edge_kind(e); + int ia = graph_vertex_index(verts, e->data.function.args[0]); + int ib = graph_vertex_index(verts, e->data.function.args[1]); + Expr* args[2] = { expr_new_integer(start + ia), expr_new_integer(start + ib) }; + ec[i] = expr_new_function(expr_new_symbol(kind), args, 2); + } + + Expr* vlist = expr_new_function(expr_new_symbol(SYM_List), vc, n); + Expr* elist = expr_new_function(expr_new_symbol(SYM_List), ec, m); + free(vc); free(ec); + Expr* gargs[2] = { vlist, elist }; + return expr_new_function(expr_new_symbol(SYM_Graph), gargs, 2); +} diff --git a/src/graph/katzcentrality.c b/src/graph/katzcentrality.c new file mode 100644 index 00000000..9e696947 --- /dev/null +++ b/src/graph/katzcentrality.c @@ -0,0 +1,79 @@ +/* katzcentrality.c - KatzCentrality[g, alpha]: the Katz centrality of each + * vertex with attenuation factor alpha (and base weight beta = 1). A vertex is + * central if it is pointed to by central vertices, with the influence of a + * length-k walk discounted by alpha^k: + * + * x_i = alpha * sum_{j -> i} x_j + 1, i.e. (I - alpha A^T) x = 1. + * + * We assemble that linear system and solve it exactly through LinearSolve, so a + * rational alpha yields an exact rational centrality vector. A^T uses in-edges + * (predecessors), so for a directed graph a vertex's score depends on who points + * at it; for an undirected graph in- and out-neighbourhoods coincide. alpha must + * be a number (integer, rational, or real); the solve fails -- leaving the call + * unevaluated -- when I - alpha A^T is singular (alpha at a reciprocal + * eigenvalue). O(V^3) from the solve. + * + * Memory (SPEC section 4): returns a fresh vector; frees res. NULL on a + * non-graph, a missing/non-numeric alpha, or a singular system. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include "eval.h" +#include +#include + +/* True if e is a plain numeric literal usable as alpha. */ +static int is_number(const Expr* e) { + if (e->type == EXPR_INTEGER || e->type == EXPR_REAL || e->type == EXPR_BIGINT) + return 1; + if (e->type == EXPR_FUNCTION && e->data.function.head && + e->data.function.head->type == EXPR_SYMBOL && + strcmp(e->data.function.head->data.symbol, "Rational") == 0) + return 1; + return 0; +} + +Expr* builtin_katz_centrality(Expr* res) { + if (res->data.function.arg_count != 2) return NULL; + const Expr* alpha = res->data.function.args[1]; + if (!is_number(alpha)) return NULL; + GraphAdj* a = graph_build_adj(res->data.function.args[0]); + if (!a) return NULL; + int n = a->n; + if (n == 0) { graph_adj_free(a); return expr_new_function(expr_new_symbol(SYM_List), NULL, 0); } + + /* Predecessor adjacency: inadj[i][j] = 1 iff j -> i. */ + char* inadj = calloc((size_t)n * (size_t)n, 1); + for (int i = 0; i < n; i++) + for (int j = 0; j < a->indeg[i]; j++) inadj[(size_t)i * n + a->in[i][j]] = 1; + + Expr** rows = calloc((size_t)n, sizeof(Expr*)); + Expr** bvec = calloc((size_t)n, sizeof(Expr*)); + for (int i = 0; i < n; i++) { + Expr** row = calloc((size_t)n, sizeof(Expr*)); + for (int j = 0; j < n; j++) { + if (i == j) { + row[j] = expr_new_integer(1); /* diagonal (no self-loops) */ + } else if (inadj[(size_t)i * n + j]) { + Expr* t[2] = { expr_new_integer(-1), expr_copy((Expr*)alpha) }; + row[j] = expr_new_function(expr_new_symbol(SYM_Times), t, 2); /* -alpha */ + } else { + row[j] = expr_new_integer(0); + } + } + rows[i] = expr_new_function(expr_new_symbol(SYM_List), row, (size_t)n); + free(row); + bvec[i] = expr_new_integer(1); /* beta = 1 */ + } + free(inadj); + graph_adj_free(a); + + Expr* matrix = expr_new_function(expr_new_symbol(SYM_List), rows, (size_t)n); + Expr* vec = expr_new_function(expr_new_symbol(SYM_List), bvec, (size_t)n); + free(rows); free(bvec); + Expr* args[2] = { matrix, vec }; + Expr* solve = expr_new_function(expr_new_symbol("LinearSolve"), args, 2); + return evaluate(solve); +} diff --git a/src/graph/kcore.c b/src/graph/kcore.c new file mode 100644 index 00000000..4775ea4f --- /dev/null +++ b/src/graph/kcore.c @@ -0,0 +1,99 @@ +/* kcore.c - KCoreComponents[g, k]: the connected components of the k-core of g, + * as a list of vertex lists. The k-core is the maximal subgraph in which every + * vertex has (undirected) degree at least k; it is found by repeatedly deleting + * any vertex whose current degree drops below k until none remain. + * + * Peeling is O(V + E): each vertex is queued for removal at most once, and each + * edge is examined a constant number of times as its endpoints are deleted. The + * survivors are then split into connected components by BFS. Edge direction is + * ignored (the k-core is defined on the underlying undirected graph), matching + * Wolfram; components come out ordered by least vertex index, vertices within a + * component in canonical order. + * + * k must be a non-negative integer literal, else the call is left unevaluated. + * + * Memory (SPEC section 4): returns a fresh List of Lists; frees res. NULL on a + * non-graph argument. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +Expr* builtin_kcore_components(Expr* res) { + if (res->data.function.arg_count != 2) return NULL; + const Expr* g = res->data.function.args[0]; + const Expr* ke = res->data.function.args[1]; + if (!graph_is_valid(g)) return NULL; + if (ke->type != EXPR_INTEGER || ke->data.integer < 0) return NULL; + long k = (long)ke->data.integer; + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + int n = (int)verts->data.function.arg_count; + size_t ne = edges->data.function.arg_count; + + /* Symmetric boolean adjacency (direction ignored) + degrees. */ + char* adj = (n > 0) ? calloc((size_t)n * (size_t)n, 1) : NULL; + int* deg = (n > 0) ? calloc((size_t)n, sizeof(int)) : NULL; + for (size_t e = 0; e < ne; e++) { + const Expr* ed = edges->data.function.args[e]; + int ia = graph_vertex_index(verts, ed->data.function.args[0]); + int ib = graph_vertex_index(verts, ed->data.function.args[1]); + if (ia < 0 || ib < 0 || ia == ib) continue; + if (!adj[(size_t)ia * n + ib]) { /* first time this pair */ + adj[(size_t)ia * n + ib] = adj[(size_t)ib * n + ia] = 1; + deg[ia]++; deg[ib]++; + } + } + + /* Peel: remove any alive vertex whose degree < k, cascading. */ + char* alive = (n > 0) ? malloc((size_t)n) : NULL; + for (int i = 0; i < n; i++) alive[i] = 1; + int* queue = (n > 0) ? malloc((size_t)n * sizeof(int)) : NULL; + int qh = 0, qt = 0; + for (int i = 0; i < n; i++) if (deg[i] < k) queue[qt++] = i; + while (qh < qt) { + int v = queue[qh++]; + if (!alive[v]) continue; + alive[v] = 0; + for (int u = 0; u < n; u++) + if (adj[(size_t)v * n + u] && alive[u] && --deg[u] == k - 1) + queue[qt++] = u; /* enqueue once, on the downward crossing */ + } + + /* BFS the survivors into components, ordered by least vertex index. */ + char* seen = (n > 0) ? calloc((size_t)n, 1) : NULL; + Expr** comps = (n > 0) ? calloc((size_t)n, sizeof(Expr*)) : NULL; + size_t ncomp = 0; + for (int s = 0; s < n; s++) { + if (!alive[s] || seen[s]) continue; + int bh = 0, bt = 0; + queue[bt++] = s; seen[s] = 1; + int* members = malloc((size_t)n * sizeof(int)); + int mc = 0; + while (bh < bt) { + int v = queue[bh++]; + members[mc++] = v; + for (int u = 0; u < n; u++) + if (adj[(size_t)v * n + u] && alive[u] && !seen[u]) { seen[u] = 1; queue[bt++] = u; } + } + /* members collected in BFS order; emit in canonical (index) order. */ + Expr** vs = calloc((size_t)mc, sizeof(Expr*)); + int idx = 0; + for (int v = 0; v < n; v++) { + int in_comp = 0; + for (int m = 0; m < mc; m++) if (members[m] == v) { in_comp = 1; break; } + if (in_comp) vs[idx++] = expr_copy(verts->data.function.args[v]); + } + comps[ncomp++] = expr_new_function(expr_new_symbol(SYM_List), vs, (size_t)mc); + free(vs); free(members); + } + + free(adj); free(deg); free(alive); free(queue); free(seen); + + Expr* out = expr_new_function(expr_new_symbol(SYM_List), comps, ncomp); + free(comps); + return out; +} diff --git a/src/graph/kirchhoff.c b/src/graph/kirchhoff.c new file mode 100644 index 00000000..0a6cadf6 --- /dev/null +++ b/src/graph/kirchhoff.c @@ -0,0 +1,52 @@ +/* kirchhoff.c - KirchhoffMatrix[g]: the graph Laplacian L = D - A. + * + * D is the diagonal matrix of vertex degrees (row sums of the adjacency matrix: + * the degree for an undirected graph, the out-degree for a directed one) and A + * is the 0/1 adjacency matrix. So L[i][i] = deg(i) and L[i][j] = -1 when there + * is an edge i->j, else 0. Every row sums to 0. + * + * The Laplacian is the bridge to spectral graph theory: it is symmetric for an + * undirected graph, the multiplicity of its zero eigenvalue is the number of + * connected components, and (Matrix-Tree theorem) any cofactor equals the + * number of spanning trees — all reachable via the existing dense linear + * algebra (Eigenvalues, Det, ...) since the result is an ordinary matrix. + * + * O(V^2) to materialize the dense matrix. Memory (SPEC section 4): returns a + * fresh List-of-Lists; frees res. NULL (unevaluated) on a non-graph argument. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +Expr* builtin_kirchhoff_matrix(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + GraphAdj* a = graph_build_adj(res->data.function.args[0]); + if (!a) return NULL; + int n = a->n; + + /* Adjacency membership: adj[i*n+j] = 1 iff edge i->j. */ + char* adj = (n > 0) ? calloc((size_t)n * (size_t)n, 1) : NULL; + if (n > 0 && !adj) { graph_adj_free(a); return NULL; } + for (int i = 0; i < n; i++) + for (int e = 0; e < a->outdeg[i]; e++) + adj[(size_t)i * n + a->out[i][e]] = 1; + + Expr** rows = (n > 0) ? calloc((size_t)n, sizeof(Expr*)) : NULL; + for (int i = 0; i < n; i++) { + Expr** cells = (n > 0) ? calloc((size_t)n, sizeof(Expr*)) : NULL; + for (int j = 0; j < n; j++) { + int v = (i == j) ? a->outdeg[i] + : (adj[(size_t)i * n + j] ? -1 : 0); + cells[j] = expr_new_integer(v); + } + rows[i] = expr_new_function(expr_new_symbol(SYM_List), cells, (size_t)n); + free(cells); + } + free(adj); + Expr* out = expr_new_function(expr_new_symbol(SYM_List), rows, (size_t)n); + free(rows); + graph_adj_free(a); + return out; +} diff --git a/src/graph/kneser.c b/src/graph/kneser.c new file mode 100644 index 00000000..7982f43b --- /dev/null +++ b/src/graph/kneser.c @@ -0,0 +1,77 @@ +/* kneser.c - KneserGraph[n, k]: the Kneser graph K(n, k). Its vertices are the + * k-element subsets of {1, ..., n}, and two are adjacent iff the subsets are + * disjoint. + * + * Vertices are C(n, k) subsets, each labelled by the sorted List of its elements; + * disjointness is tested in O(1) with element bitmasks (so n is capped at 62). + * O(C(n,k)^2). Special cases: K(n, 1) is the complete graph K_n, K(5, 2) is the + * Petersen graph (10 vertices, 15 edges, 3-regular), K(2k, k) is a perfect + * matching. + * + * n >= 0, 0 <= k <= n integers; left unevaluated if the vertex count would exceed + * a safety cap. + * + * Memory (SPEC section 4): returns a freshly-built Graph; frees res. NULL on bad + * arguments or an oversized graph. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +#define KNESER_MAX_VERTICES 3000 + +Expr* builtin_kneser_graph(Expr* res) { + if (res->data.function.arg_count != 2) return NULL; + const Expr* ne = res->data.function.args[0]; + const Expr* ke = res->data.function.args[1]; + if (ne->type != EXPR_INTEGER || ke->type != EXPR_INTEGER) return NULL; + long n = (long)ne->data.integer, k = (long)ke->data.integer; + if (n < 0 || k < 0 || k > n || n > 62) return NULL; + + /* Vertex count C(n, k), with overflow/size guard. */ + long C = 1; + for (long i = 0; i < k; i++) { + C = C * (n - i) / (i + 1); + if (C > KNESER_MAX_VERTICES) return NULL; + } + + /* Enumerate k-subsets in lexicographic order: labels + element bitmasks. */ + Expr** vc = malloc((size_t)(C > 0 ? C : 1) * sizeof(Expr*)); + unsigned long long* mask = malloc((size_t)(C > 0 ? C : 1) * sizeof(unsigned long long)); + long* idx = malloc((size_t)(k > 0 ? k : 1) * sizeof(long)); + for (long i = 0; i < k; i++) idx[i] = i; /* 0-based elements */ + for (long v = 0; v < C; v++) { + Expr** els = malloc((size_t)(k > 0 ? k : 1) * sizeof(Expr*)); + unsigned long long m = 0; + for (long i = 0; i < k; i++) { els[i] = expr_new_integer(idx[i] + 1); m |= 1ULL << idx[i]; } + vc[v] = expr_new_function(expr_new_symbol(SYM_List), els, (size_t)k); + mask[v] = m; + free(els); + /* Advance to the next k-combination. */ + long p = k - 1; + while (p >= 0 && idx[p] == n - k + p) p--; + if (p < 0) break; + idx[p]++; + for (long i = p + 1; i < k; i++) idx[i] = idx[i - 1] + 1; + } + free(idx); + + /* Edges: disjoint subsets (mask & mask == 0), distinct vertices. */ + Expr** ec = NULL; size_t ecap = 0, me = 0; + for (long i = 0; i < C; i++) + for (long j = i + 1; j < C; j++) + if ((mask[i] & mask[j]) == 0) { + if (me == ecap) { ecap = ecap ? ecap * 2 : 16; ec = realloc(ec, ecap * sizeof(Expr*)); } + Expr* a[2] = { expr_copy(vc[i]), expr_copy(vc[j]) }; + ec[me++] = expr_new_function(expr_new_symbol(SYM_UndirectedEdge), a, 2); + } + free(mask); + + Expr* vlist = expr_new_function(expr_new_symbol(SYM_List), vc, (size_t)C); + Expr* elist = expr_new_function(expr_new_symbol(SYM_List), ec, me); + free(vc); free(ec); + Expr* gargs[2] = { vlist, elist }; + return expr_new_function(expr_new_symbol(SYM_Graph), gargs, 2); +} diff --git a/src/graph/laddergraph.c b/src/graph/laddergraph.c new file mode 100644 index 00000000..9d9f92cb --- /dev/null +++ b/src/graph/laddergraph.c @@ -0,0 +1,50 @@ +/* laddergraph.c - LadderGraph[n]: the ladder graph L_n, two paths on n vertices + * ("rails") joined by n "rungs" -- the Cartesian product P_n [] P_2. + * + * Vertices are 1..2n: the top rail 1..n and the bottom rail n+1..2n. Edges are + * the two rail paths (i,i+1) and (n+i,n+i+1) for i = 1..n-1, plus the rungs + * (i, n+i) for i = 1..n -- 3n-2 edges in all. O(n). L_1 is a single edge, L_2 is + * C_4 (a square). + * + * Memory (SPEC section 4): returns a freshly-built Graph; frees res. NULL unless + * n >= 1 is an integer. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +Expr* builtin_ladder_graph(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* ne = res->data.function.args[0]; + if (ne->type != EXPR_INTEGER) return NULL; + long n = (long)ne->data.integer; + if (n < 1) return NULL; + + long V = 2 * n; + Expr** vc = malloc((size_t)V * sizeof(Expr*)); + for (long i = 0; i < V; i++) vc[i] = expr_new_integer(i + 1); + + size_t total = (size_t)(3 * n - 2); + Expr** ec = malloc(total * sizeof(Expr*)); + size_t me = 0; + for (long i = 1; i < n; i++) { /* top rail */ + Expr* a[2] = { expr_new_integer(i), expr_new_integer(i + 1) }; + ec[me++] = expr_new_function(expr_new_symbol(SYM_UndirectedEdge), a, 2); + } + for (long i = 1; i < n; i++) { /* bottom rail */ + Expr* a[2] = { expr_new_integer(n + i), expr_new_integer(n + i + 1) }; + ec[me++] = expr_new_function(expr_new_symbol(SYM_UndirectedEdge), a, 2); + } + for (long i = 1; i <= n; i++) { /* rungs */ + Expr* a[2] = { expr_new_integer(i), expr_new_integer(n + i) }; + ec[me++] = expr_new_function(expr_new_symbol(SYM_UndirectedEdge), a, 2); + } + + Expr* vlist = expr_new_function(expr_new_symbol(SYM_List), vc, (size_t)V); + Expr* elist = expr_new_function(expr_new_symbol(SYM_List), ec, me); + free(vc); free(ec); + Expr* gargs[2] = { vlist, elist }; + return expr_new_function(expr_new_symbol(SYM_Graph), gargs, 2); +} diff --git a/src/graph/layout.c b/src/graph/layout.c new file mode 100644 index 00000000..86c8de3f --- /dev/null +++ b/src/graph/layout.c @@ -0,0 +1,553 @@ +/* layout.c - vertex-coordinate layouts for GraphPlot / HighlightGraph. + * + * graph_compute_layout() fills caller-allocated x[]/y[] (length n) with 2D + * coordinates for a validated graph under a named layout, roughly normalized + * to the [-1, 1] box. Every kernel is fully deterministic (no RNG state, no + * Date/random) so a notebook re-run reproduces the same picture. + * + * We implement a compact set of geometric + force-directed kernels and map the + * full Wolfram-Language GraphLayout name list onto them (see layout_kernel()). + * Members of the energy-minimization family (SpringElectrical, Gravity, + * Spectral, Tutte, Planar, ...) are all served by a Fruchterman-Reingold + * spring-electrical solver; tree/layer families share a BFS-layered kernel; + * partite families share a two-column kernel. Names we approximate rather than + * reproduce exactly are documented in docs/spec/builtins/graphs.md. + * + * Memory: read-only over g; allocates a transient GraphAdj for the kernels that + * need adjacency and frees it before returning. + */ + +#include "graph.h" +#include "expr.h" +#include +#include +#include + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +typedef enum { + LAYOUT_CIRCULAR, + LAYOUT_SPRING, /* Fruchterman-Reingold spring-electrical */ + LAYOUT_GRAVITY, /* FR with an added central gravity well */ + LAYOUT_HIGHDIM, /* two-pivot BFS-distance embedding projected to 2D */ + LAYOUT_HYPERBOLIC, /* FR then radial warp onto a Poincare-like disk */ + LAYOUT_SPIRAL, + LAYOUT_LINEAR, + LAYOUT_GRID, + LAYOUT_RANDOM, + LAYOUT_STAR, + LAYOUT_RADIAL, /* concentric BFS shells from a root */ + LAYOUT_LAYERED, /* stacked BFS layers */ + LAYOUT_BIPARTITE /* two columns from a BFS 2-coloring */ +} LayoutKernel; + +/* Case-sensitive exact match against the Wolfram GraphLayout name list, mapping + * each to the kernel that best serves it. Unknown / None -> circular. */ +static LayoutKernel layout_kernel(const char* name) { + if (!name || !*name) return LAYOUT_CIRCULAR; + struct { const char* n; LayoutKernel k; } table[] = { + { "CircularEmbedding", LAYOUT_CIRCULAR }, + { "SpringElectricalEmbedding", LAYOUT_SPRING }, + { "SpringEmbedding", LAYOUT_SPRING }, + { "GravityEmbedding", LAYOUT_GRAVITY }, + { "HighDimensionalEmbedding", LAYOUT_HIGHDIM }, + { "SpectralEmbedding", LAYOUT_HIGHDIM }, + { "SphericalEmbedding", LAYOUT_HYPERBOLIC}, + { "HyperbolicSpringEmbedding", LAYOUT_HYPERBOLIC}, + { "TutteEmbedding", LAYOUT_SPRING }, + { "PlanarEmbedding", LAYOUT_SPRING }, + { "SpiralEmbedding", LAYOUT_SPIRAL }, + { "DiscreteSpiralEmbedding", LAYOUT_SPIRAL }, + { "LinearEmbedding", LAYOUT_LINEAR }, + { "GridEmbedding", LAYOUT_GRID }, + { "RandomEmbedding", LAYOUT_RANDOM }, + { "StarEmbedding", LAYOUT_STAR }, + { "RadialEmbedding", LAYOUT_RADIAL }, + { "BalloonEmbedding", LAYOUT_RADIAL }, + { "HyperbolicRadialEmbedding", LAYOUT_RADIAL }, + { "LayeredEmbedding", LAYOUT_LAYERED }, + { "LayeredDigraphEmbedding", LAYOUT_LAYERED }, + { "SymmetricLayeredEmbedding", LAYOUT_LAYERED }, + { "BipartiteEmbedding", LAYOUT_BIPARTITE }, + { "MultipartiteEmbedding", LAYOUT_BIPARTITE }, + { "CircularMultipartiteEmbedding",LAYOUT_BIPARTITE }, + }; + for (size_t i = 0; i < sizeof(table) / sizeof(table[0]); i++) + if (strcmp(name, table[i].n) == 0) return table[i].k; + return LAYOUT_CIRCULAR; +} + +/* Rescale coordinates to fit the [-1, 1] box while preserving aspect ratio. */ +static void normalize(double* x, double* y, int n) { + if (n <= 0) return; + double minx = x[0], maxx = x[0], miny = y[0], maxy = y[0]; + for (int i = 1; i < n; i++) { + if (x[i] < minx) minx = x[i]; + if (x[i] > maxx) maxx = x[i]; + if (y[i] < miny) miny = y[i]; + if (y[i] > maxy) maxy = y[i]; + } + double cx = 0.5 * (minx + maxx), cy = 0.5 * (miny + maxy); + double span = fmax(maxx - minx, maxy - miny); + double s = (span > 1e-9) ? 2.0 / span : 1.0; /* map larger span to [-1,1] */ + for (int i = 0; i < n; i++) { + x[i] = (x[i] - cx) * s; + y[i] = (y[i] - cy) * s; + } +} + +static void layout_circular(double* x, double* y, int n) { + for (int i = 0; i < n; i++) { + if (n == 1) { x[i] = 0.0; y[i] = 0.0; continue; } + double t = 2.0 * M_PI * (double)i / (double)n; + x[i] = cos(t); y[i] = sin(t); + } +} + +static void layout_linear(double* x, double* y, int n) { + for (int i = 0; i < n; i++) { + x[i] = (n == 1) ? 0.0 : (-1.0 + 2.0 * (double)i / (double)(n - 1)); + y[i] = 0.0; + } +} + +static void layout_spiral(double* x, double* y, int n) { + /* Archimedean spiral: radius grows linearly with a constant angular step. */ + for (int i = 0; i < n; i++) { + double t = (double)i * 0.6; /* angular step (radians) */ + double r = (n <= 1) ? 0.0 : (double)i / (double)(n - 1); + x[i] = r * cos(t); y[i] = r * sin(t); + } +} + +static void layout_grid(double* x, double* y, int n) { + int cols = (int)ceil(sqrt((double)n)); + if (cols < 1) cols = 1; + for (int i = 0; i < n; i++) { + int r = i / cols, c = i % cols; + x[i] = (double)c; y[i] = -(double)r; /* row 0 on top */ + } +} + +static void layout_random(double* x, double* y, int n) { + /* Deterministic pseudo-random: a fixed LCG folded over the vertex index, + * so the picture is stable across runs (no global RNG dependence). */ + unsigned long s = 0x9E3779B9UL; + for (int i = 0; i < n; i++) { + s = s * 1103515245UL + 12345UL + (unsigned long)(i + 1) * 2654435761UL; + double ux = (double)((s >> 16) & 0x7FFF) / 32767.0; + s = s * 1103515245UL + 12345UL; + double uy = (double)((s >> 16) & 0x7FFF) / 32767.0; + x[i] = 2.0 * ux - 1.0; y[i] = 2.0 * uy - 1.0; + } +} + +static void layout_star(const GraphAdj* a, double* x, double* y, int n) { + /* Highest-degree vertex at the center; the rest evenly on a circle. */ + int hub = 0, best = -1; + for (int i = 0; i < n; i++) { + int deg = a ? (a->outdeg[i] + a->indeg[i]) : 0; + if (deg > best) { best = deg; hub = i; } + } + int placed = 0, rim = (n > 1) ? n - 1 : 1; + for (int i = 0; i < n; i++) { + if (i == hub) { x[i] = 0.0; y[i] = 0.0; continue; } + double t = 2.0 * M_PI * (double)placed / (double)rim; + x[i] = cos(t); y[i] = sin(t); placed++; + } +} + +/* BFS distance labels from the highest-degree vertex, over the underlying + * undirected graph. dist[i] = shell index; unreached vertices get maxd+1. */ +static int* bfs_levels(const GraphAdj* a, int n, int* maxd_out) { + int* dist = malloc((size_t)(n > 0 ? n : 1) * sizeof(int)); + int* queue = malloc((size_t)(n > 0 ? n : 1) * sizeof(int)); + if (!dist || !queue) { free(dist); free(queue); *maxd_out = 0; return NULL; } + for (int i = 0; i < n; i++) dist[i] = -1; + int root = 0, best = -1; + for (int i = 0; i < n; i++) { + int deg = a->outdeg[i] + a->indeg[i]; + if (deg > best) { best = deg; root = i; } + } + int head = 0, tail = 0, maxd = 0; + dist[root] = 0; queue[tail++] = root; + while (head < tail) { + int u = queue[head++]; + if (dist[u] > maxd) maxd = dist[u]; + for (int e = 0; e < a->outdeg[u]; e++) { + int v = a->out[u][e]; + if (dist[v] < 0) { dist[v] = dist[u] + 1; queue[tail++] = v; } + } + for (int e = 0; e < a->indeg[u]; e++) { + int v = a->in[u][e]; + if (dist[v] < 0) { dist[v] = dist[u] + 1; queue[tail++] = v; } + } + } + for (int i = 0; i < n; i++) if (dist[i] < 0) dist[i] = maxd + 1; /* disconnected */ + int any_disc = 0; + for (int i = 0; i < n; i++) if (dist[i] == maxd + 1) { any_disc = 1; break; } + if (any_disc) maxd += 1; + free(queue); + *maxd_out = maxd; + return dist; +} + +static void layout_radial(const GraphAdj* a, double* x, double* y, int n) { + int maxd = 0; + int* dist = bfs_levels(a, n, &maxd); + if (!dist) { layout_circular(x, y, n); return; } + int* count = calloc((size_t)(maxd + 2), sizeof(int)); + int* seen = calloc((size_t)(maxd + 2), sizeof(int)); + for (int i = 0; i < n; i++) count[dist[i]]++; + for (int i = 0; i < n; i++) { + int d = dist[i]; + double r = (maxd == 0) ? 0.0 : (double)d / (double)maxd; + int c = count[d]; + double t = (c > 0) ? 2.0 * M_PI * (double)seen[d] / (double)c : 0.0; + seen[d]++; + x[i] = r * cos(t); y[i] = r * sin(t); + } + free(count); free(seen); free(dist); +} + +static void layout_layered(const GraphAdj* a, double* x, double* y, int n) { + int maxd = 0; + int* dist = bfs_levels(a, n, &maxd); + if (!dist) { layout_circular(x, y, n); return; } + int* count = calloc((size_t)(maxd + 2), sizeof(int)); + int* seen = calloc((size_t)(maxd + 2), sizeof(int)); + for (int i = 0; i < n; i++) count[dist[i]]++; + for (int i = 0; i < n; i++) { + int d = dist[i], c = count[d]; + x[i] = (c <= 1) ? 0.0 : (-1.0 + 2.0 * (double)seen[d] / (double)(c - 1)); + y[i] = -(double)d; + seen[d]++; + } + free(count); free(seen); free(dist); +} + +static void layout_bipartite(const GraphAdj* a, double* x, double* y, int n) { + /* 2-color the underlying undirected graph by BFS parity; place color 0 in + * the left column and color 1 in the right. Non-bipartite graphs still get + * a legible two-column split (parity is well defined per BFS tree). */ + int* color = malloc((size_t)(n > 0 ? n : 1) * sizeof(int)); + int* queue = malloc((size_t)(n > 0 ? n : 1) * sizeof(int)); + if (!color || !queue) { free(color); free(queue); layout_circular(x, y, n); return; } + for (int i = 0; i < n; i++) color[i] = -1; + for (int s = 0; s < n; s++) { + if (color[s] >= 0) continue; + int head = 0, tail = 0; + color[s] = 0; queue[tail++] = s; + while (head < tail) { + int u = queue[head++]; + for (int e = 0; e < a->outdeg[u]; e++) { + int v = a->out[u][e]; + if (color[v] < 0) { color[v] = 1 - color[u]; queue[tail++] = v; } + } + for (int e = 0; e < a->indeg[u]; e++) { + int v = a->in[u][e]; + if (color[v] < 0) { color[v] = 1 - color[u]; queue[tail++] = v; } + } + } + } + int c0 = 0, c1 = 0; + for (int i = 0; i < n; i++) { if (color[i] == 0) c0++; else c1++; } + int s0 = 0, s1 = 0; + for (int i = 0; i < n; i++) { + if (color[i] == 0) { + x[i] = -1.0; + y[i] = (c0 <= 1) ? 0.0 : (-1.0 + 2.0 * (double)s0++ / (double)(c0 - 1)); + } else { + x[i] = 1.0; + y[i] = (c1 <= 1) ? 0.0 : (-1.0 + 2.0 * (double)s1++ / (double)(c1 - 1)); + } + } + free(color); free(queue); +} + +/* Fruchterman-Reingold spring-electrical core. Vertices repel each other + * (~k^2/d); adjacent vertices attract (~d^2/k). When `gravity` > 0 an extra + * central pull (~gravity*distance-from-origin, scaled by 1+degree) compacts the + * graph and draws well-connected vertices inward — this is what distinguishes + * GravityEmbedding from the plain spring-electrical layout. Initialized from the + * circular layout (deterministic) and cooled linearly, so the result is stable. + */ +static void fr_core(const GraphAdj* a, double* x, double* y, int n, double gravity) { + if (n <= 2) { layout_circular(x, y, n); return; } + layout_circular(x, y, n); /* deterministic seed */ + double area = 4.0; /* the [-1,1]^2 box */ + double k = sqrt(area / (double)n); /* natural edge length */ + double* dx = calloc((size_t)n, sizeof(double)); + double* dy = calloc((size_t)n, sizeof(double)); + if (!dx || !dy) { free(dx); free(dy); return; } + int iters = 300; + double t = 0.20; /* initial max displacement */ + double cool = t / (double)(iters + 1); + for (int it = 0; it < iters; it++) { + for (int i = 0; i < n; i++) { dx[i] = 0.0; dy[i] = 0.0; } + /* Repulsion between every pair. */ + for (int i = 0; i < n; i++) { + for (int j = i + 1; j < n; j++) { + double ddx = x[i] - x[j], ddy = y[i] - y[j]; + double d2 = ddx * ddx + ddy * ddy; + if (d2 < 1e-9) { ddx = 1e-3 * (i - j); ddy = 1e-3; d2 = ddx*ddx+ddy*ddy; } + double d = sqrt(d2); + double f = (k * k) / d; + double ux = ddx / d, uy = ddy / d; + dx[i] += ux * f; dy[i] += uy * f; + dx[j] -= ux * f; dy[j] -= uy * f; + } + } + /* Attraction along edges (undirected: use out-adjacency, each edge once + * per stored direction — symmetric contributions balance out). */ + for (int u = 0; u < n; u++) { + for (int e = 0; e < a->outdeg[u]; e++) { + int v = a->out[u][e]; + double ddx = x[u] - x[v], ddy = y[u] - y[v]; + double d = sqrt(ddx * ddx + ddy * ddy); + if (d < 1e-9) continue; + double f = (d * d) / k; + double ux = ddx / d, uy = ddy / d; + dx[u] -= ux * f; dy[u] -= uy * f; + dx[v] += ux * f; dy[v] += uy * f; + } + } + /* Optional central gravity: pull toward the origin, stronger for + * higher-degree vertices, so hubs settle in the middle. */ + if (gravity > 0.0) { + for (int i = 0; i < n; i++) { + double mass = 1.0 + 0.5 * (double)(a->outdeg[i] + a->indeg[i]); + dx[i] -= gravity * mass * x[i]; + dy[i] -= gravity * mass * y[i]; + } + } + /* Displace, capped by the current temperature. */ + for (int i = 0; i < n; i++) { + double d = sqrt(dx[i] * dx[i] + dy[i] * dy[i]); + if (d > 1e-9) { + double cap = fmin(d, t); + x[i] += (dx[i] / d) * cap; + y[i] += (dy[i] / d) * cap; + } + } + t -= cool; + } + free(dx); free(dy); +} + +static void layout_spring(const GraphAdj* a, double* x, double* y, int n) { + fr_core(a, x, y, n, 0.0); +} + +static void layout_gravity(const GraphAdj* a, double* x, double* y, int n) { + fr_core(a, x, y, n, 0.12); +} + +/* HyperbolicSpring / Spherical: run the spring layout, then warp radii outward + * so vertices crowd toward the boundary of a disk (a Poincare-disk feel). */ +static void layout_hyperbolic(const GraphAdj* a, double* x, double* y, int n) { + fr_core(a, x, y, n, 0.04); + if (n <= 1) return; + double cx = 0.0, cy = 0.0; + for (int i = 0; i < n; i++) { cx += x[i]; cy += y[i]; } + cx /= n; cy /= n; + double maxr = 1e-9; + for (int i = 0; i < n; i++) { + double r = hypot(x[i] - cx, y[i] - cy); + if (r > maxr) maxr = r; + } + for (int i = 0; i < n; i++) { + double ddx = x[i] - cx, ddy = y[i] - cy; + double r = hypot(ddx, ddy); + if (r < 1e-9) continue; + double rn = r / maxr; /* in [0,1] */ + double warped = pow(rn, 0.45); /* push outward toward the rim */ + double s = warped / rn; + x[i] = cx + ddx * s; + y[i] = cy + ddy * s; + } +} + +/* BFS distances from `src` over the underlying undirected graph; unreached + * vertices get `n` (a value larger than any real distance). */ +static void bfs_from(const GraphAdj* a, int n, int src, int* dist) { + int* queue = malloc((size_t)(n > 0 ? n : 1) * sizeof(int)); + if (!queue) { for (int i = 0; i < n; i++) dist[i] = 0; return; } + for (int i = 0; i < n; i++) dist[i] = -1; + int head = 0, tail = 0; + dist[src] = 0; queue[tail++] = src; + while (head < tail) { + int u = queue[head++]; + for (int e = 0; e < a->outdeg[u]; e++) { + int v = a->out[u][e]; + if (dist[v] < 0) { dist[v] = dist[u] + 1; queue[tail++] = v; } + } + for (int e = 0; e < a->indeg[u]; e++) { + int v = a->in[u][e]; + if (dist[v] < 0) { dist[v] = dist[u] + 1; queue[tail++] = v; } + } + } + for (int i = 0; i < n; i++) if (dist[i] < 0) dist[i] = n; + free(queue); +} + +static int farthest(const int* dist, int n) { + int best = 0, bd = -1; + for (int i = 0; i < n; i++) { + int d = (dist[i] >= n) ? -1 : dist[i]; /* ignore unreached */ + if (d > bd) { bd = d; best = i; } + } + return best; +} + +/* HighDimensional / Spectral (approx): embed each vertex by its BFS distance to + * two far-apart pivots (found by a double sweep) and use those two distances as + * the 2D coordinates — a cheap pivot-MDS that lays the graph out along its + * "diameter". A tiny index-based nudge separates coincident vertices. */ +static void layout_highdim(const GraphAdj* a, double* x, double* y, int n) { + if (n <= 2) { layout_circular(x, y, n); return; } + int* d = malloc((size_t)n * sizeof(int)); + int* d1 = malloc((size_t)n * sizeof(int)); + int* d2 = malloc((size_t)n * sizeof(int)); + if (!d || !d1 || !d2) { free(d); free(d1); free(d2); layout_circular(x, y, n); return; } + bfs_from(a, n, 0, d); int p1 = farthest(d, n); + bfs_from(a, n, p1, d1); int p2 = farthest(d1, n); + bfs_from(a, n, p2, d2); + for (int i = 0; i < n; i++) { + double a1 = (d1[i] >= n) ? 0.0 : (double)d1[i]; + double a2 = (d2[i] >= n) ? 0.0 : (double)d2[i]; + x[i] = a1 + 0.001 * (double)(i % 7); + y[i] = a2 + 0.001 * (double)(i % 5); + } + free(d); free(d1); free(d2); +} + +void graph_compute_layout(const Expr* g, const char* layout, double* x, double* y) { + const Expr* verts = g->data.function.args[0]; + int n = (int)verts->data.function.arg_count; + if (n <= 0) return; + + LayoutKernel kern = layout_kernel(layout); + + /* Kernels that need adjacency build it once; geometric kernels don't. */ + GraphAdj* a = NULL; + if (kern == LAYOUT_SPRING || kern == LAYOUT_GRAVITY || kern == LAYOUT_HIGHDIM + || kern == LAYOUT_HYPERBOLIC || kern == LAYOUT_STAR || kern == LAYOUT_RADIAL + || kern == LAYOUT_LAYERED || kern == LAYOUT_BIPARTITE) { + a = graph_build_adj(g); + if (!a) kern = LAYOUT_CIRCULAR; /* defensive: fall back if build fails */ + } + + switch (kern) { + case LAYOUT_CIRCULAR: layout_circular(x, y, n); break; + case LAYOUT_LINEAR: layout_linear(x, y, n); break; + case LAYOUT_SPIRAL: layout_spiral(x, y, n); break; + case LAYOUT_GRID: layout_grid(x, y, n); break; + case LAYOUT_RANDOM: layout_random(x, y, n); break; + case LAYOUT_STAR: layout_star(a, x, y, n); break; + case LAYOUT_RADIAL: layout_radial(a, x, y, n); break; + case LAYOUT_LAYERED: layout_layered(a, x, y, n); break; + case LAYOUT_BIPARTITE: layout_bipartite(a, x, y, n); break; + case LAYOUT_SPRING: layout_spring(a, x, y, n); break; + case LAYOUT_GRAVITY: layout_gravity(a, x, y, n); break; + case LAYOUT_HIGHDIM: layout_highdim(a, x, y, n); break; + case LAYOUT_HYPERBOLIC:layout_hyperbolic(a, x, y, n); break; + } + + if (a) graph_adj_free(a); + normalize(x, y, n); +} + +/* ---- 3D layout (Graph3D) --------------------------------------------------- */ + +static void normalize3d(double* x, double* y, double* z, int n) { + if (n <= 0) return; + double mn[3] = { x[0], y[0], z[0] }, mx[3] = { x[0], y[0], z[0] }; + for (int i = 1; i < n; i++) { + if (x[i] < mn[0]) mn[0] = x[i]; if (x[i] > mx[0]) mx[0] = x[i]; + if (y[i] < mn[1]) mn[1] = y[i]; if (y[i] > mx[1]) mx[1] = y[i]; + if (z[i] < mn[2]) mn[2] = z[i]; if (z[i] > mx[2]) mx[2] = z[i]; + } + double span = fmax(mx[0] - mn[0], fmax(mx[1] - mn[1], mx[2] - mn[2])); + double s = (span > 1e-9) ? 2.0 / span : 1.0; + double cx = 0.5*(mn[0]+mx[0]), cy = 0.5*(mn[1]+mx[1]), cz = 0.5*(mn[2]+mx[2]); + for (int i = 0; i < n; i++) { + x[i] = (x[i]-cx)*s; y[i] = (y[i]-cy)*s; z[i] = (z[i]-cz)*s; + } +} + +/* Deterministic near-uniform points on the unit sphere (golden-angle spiral). */ +static void fibonacci_sphere(double* x, double* y, double* z, int n) { + double golden = M_PI * (3.0 - sqrt(5.0)); /* golden angle */ + for (int i = 0; i < n; i++) { + double zt = (n == 1) ? 0.0 : 1.0 - 2.0 * (double)i / (double)(n - 1); + double r = sqrt(fmax(0.0, 1.0 - zt * zt)); + double th = golden * (double)i; + x[i] = r * cos(th); y[i] = r * sin(th); z[i] = zt; + } +} + +/* 3D Fruchterman-Reingold, seeded on a sphere (deterministic). */ +static void fr3d(const GraphAdj* a, double* x, double* y, double* z, int n) { + fibonacci_sphere(x, y, z, n); + if (n <= 2) return; + double k = pow(8.0 / (double)n, 1.0 / 3.0); /* natural length in a cube */ + double* dx = calloc((size_t)n, sizeof(double)); + double* dy = calloc((size_t)n, sizeof(double)); + double* dz = calloc((size_t)n, sizeof(double)); + if (!dx || !dy || !dz) { free(dx); free(dy); free(dz); return; } + int iters = 250; + double t = 0.20, cool = t / (double)(iters + 1); + for (int it = 0; it < iters; it++) { + for (int i = 0; i < n; i++) { dx[i]=dy[i]=dz[i]=0.0; } + for (int i = 0; i < n; i++) { + for (int j = i + 1; j < n; j++) { + double ax = x[i]-x[j], ay = y[i]-y[j], az = z[i]-z[j]; + double d2 = ax*ax + ay*ay + az*az; + if (d2 < 1e-9) { ax = 1e-3*(i-j); ay = 1e-3; az = 1e-3*(j-i); d2 = ax*ax+ay*ay+az*az; } + double d = sqrt(d2), f = (k*k)/d; + double ux=ax/d, uy=ay/d, uz=az/d; + dx[i]+=ux*f; dy[i]+=uy*f; dz[i]+=uz*f; + dx[j]-=ux*f; dy[j]-=uy*f; dz[j]-=uz*f; + } + } + for (int u = 0; u < n; u++) { + for (int e = 0; e < a->outdeg[u]; e++) { + int v = a->out[u][e]; + double ax = x[u]-x[v], ay = y[u]-y[v], az = z[u]-z[v]; + double d = sqrt(ax*ax+ay*ay+az*az); + if (d < 1e-9) continue; + double f = (d*d)/k, ux=ax/d, uy=ay/d, uz=az/d; + dx[u]-=ux*f; dy[u]-=uy*f; dz[u]-=uz*f; + dx[v]+=ux*f; dy[v]+=uy*f; dz[v]+=uz*f; + } + } + for (int i = 0; i < n; i++) { + double d = sqrt(dx[i]*dx[i]+dy[i]*dy[i]+dz[i]*dz[i]); + if (d > 1e-9) { + double cap = fmin(d, t); + x[i]+=(dx[i]/d)*cap; y[i]+=(dy[i]/d)*cap; z[i]+=(dz[i]/d)*cap; + } + } + t -= cool; + } + free(dx); free(dy); free(dz); +} + +void graph_compute_layout3d(const Expr* g, const char* layout, + double* x, double* y, double* z) { + const Expr* verts = g->data.function.args[0]; + int n = (int)verts->data.function.arg_count; + if (n <= 0) return; + int spherical = (layout && strcmp(layout, "SphericalEmbedding") == 0); + GraphAdj* a = graph_build_adj(g); + if (!a || spherical) { + fibonacci_sphere(x, y, z, n); + } else { + fr3d(a, x, y, z, n); + } + if (a) graph_adj_free(a); + normalize3d(x, y, z, n); +} diff --git a/src/graph/linegraph.c b/src/graph/linegraph.c new file mode 100644 index 00000000..3cb85f94 --- /dev/null +++ b/src/graph/linegraph.c @@ -0,0 +1,76 @@ +/* linegraph.c - LineGraph[g]: the line graph L(g). + * + * The vertices of L(g) are the edges of g; two are adjacent when the underlying + * edges meet: + * undirected g -> undirected L(g): edges {a,b} and {c,d} are joined iff they + * share an endpoint. + * directed g -> directed L(g): an arc a->b points to an arc c->d iff b == c + * (head of one is the tail of the next). + * Line-graph vertices are the edge expressions themselves (e.g. + * UndirectedEdge[1,2]); a simple graph has distinct edges, so they form a valid + * vertex set. + * + * O(E^2) pairwise endpoint comparison. Memory (SPEC section 4): returns the + * canonical Graph directly; frees res. NULL on a non-graph argument. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +/* True if undirected edges e,f share an endpoint. */ +static int shares_endpoint(const Expr* e, const Expr* f) { + Expr* a = e->data.function.args[0]; Expr* b = e->data.function.args[1]; + Expr* c = f->data.function.args[0]; Expr* d = f->data.function.args[1]; + return expr_eq(a, c) || expr_eq(a, d) || expr_eq(b, c) || expr_eq(b, d); +} + +Expr* builtin_line_graph(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + + const Expr* edges = g->data.function.args[1]; + size_t ne = edges->data.function.arg_count; + + int directed = (ne > 0); + for (size_t i = 0; i < ne; i++) + if (graph_edge_kind(edges->data.function.args[i]) != SYM_DirectedEdge) { directed = 0; break; } + + /* Line-graph vertices = copies of the edges. */ + Expr** lverts = (ne > 0) ? calloc(ne, sizeof(Expr*)) : NULL; + for (size_t i = 0; i < ne; i++) lverts[i] = expr_copy(edges->data.function.args[i]); + + size_t cap = directed ? ne * (ne > 0 ? ne - 1 : 0) : ne * (ne > 0 ? ne - 1 : 0) / 2; + Expr** ledges = (cap > 0) ? calloc(cap, sizeof(Expr*)) : NULL; + size_t m = 0; + + if (directed) { + for (size_t i = 0; i < ne; i++) { + const Expr* ei = edges->data.function.args[i]; + for (size_t j = 0; j < ne; j++) { + if (i == j) continue; + const Expr* ej = edges->data.function.args[j]; + if (expr_eq(ei->data.function.args[1], ej->data.function.args[0])) { + Expr* a[2] = { expr_copy((Expr*)ei), expr_copy((Expr*)ej) }; + ledges[m++] = expr_new_function(expr_new_symbol(SYM_DirectedEdge), a, 2); + } + } + } + } else { + for (size_t i = 0; i < ne; i++) + for (size_t j = i + 1; j < ne; j++) + if (shares_endpoint(edges->data.function.args[i], edges->data.function.args[j])) { + Expr* a[2] = { expr_copy(edges->data.function.args[i]), + expr_copy(edges->data.function.args[j]) }; + ledges[m++] = expr_new_function(expr_new_symbol(SYM_UndirectedEdge), a, 2); + } + } + + Expr* vlist = expr_new_function(expr_new_symbol(SYM_List), lverts, ne); + Expr* elist = expr_new_function(expr_new_symbol(SYM_List), ledges, m); + free(lverts); free(ledges); + Expr* gargs[2] = { vlist, elist }; + return expr_new_function(expr_new_symbol(SYM_Graph), gargs, 2); +} diff --git a/src/graph/meanclustering.c b/src/graph/meanclustering.c new file mode 100644 index 00000000..3f24c460 --- /dev/null +++ b/src/graph/meanclustering.c @@ -0,0 +1,71 @@ +/* meanclustering.c - MeanClusteringCoefficient[g]: the average of the local + * clustering coefficients, (1/n) * sum_v C_v, where C_v is the fraction of v's + * neighbour pairs that are adjacent (0 for deg(v) < 2). + * + * This is Mean[LocalClusteringCoefficient[g]] -- every vertex counts equally, + * including low-degree ones contributing 0. It differs from + * GlobalClusteringCoefficient (transitivity), which weights vertices by the + * number of triples they anchor. Edge direction is ignored; the value is an + * exact rational (the per-vertex terms are summed and divided by n through the + * evaluator, so the result is fully reduced). O(V * d_max^2). + * + * Memory (SPEC section 4): returns a fresh number; frees res. NULL on a + * non-graph argument. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include "eval.h" +#include + +/* Exact num/den as a reduced value (integer or Rational). den > 0 required. */ +static Expr* ratio(long num, long den) { + Expr* pa[2] = { expr_new_integer(den), expr_new_integer(-1) }; + Expr* inv = expr_new_function(expr_new_symbol(SYM_Power), pa, 2); + Expr* ta[2] = { expr_new_integer(num), inv }; + return evaluate(expr_new_function(expr_new_symbol(SYM_Times), ta, 2)); +} + +Expr* builtin_mean_clustering_coefficient(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + int n = (int)verts->data.function.arg_count; + size_t ne = edges->data.function.arg_count; + if (n == 0) return expr_new_integer(0); /* mean over no vertices */ + + char* adj = calloc((size_t)n * (size_t)n, 1); + for (size_t e = 0; e < ne; e++) { + const Expr* ed = edges->data.function.args[e]; + int ia = graph_vertex_index(verts, ed->data.function.args[0]); + int ib = graph_vertex_index(verts, ed->data.function.args[1]); + if (ia < 0 || ib < 0 || ia == ib) continue; + adj[(size_t)ia * n + ib] = adj[(size_t)ib * n + ia] = 1; + } + + Expr** terms = calloc((size_t)n, sizeof(Expr*)); + int* nbr = malloc((size_t)n * sizeof(int)); + for (int v = 0; v < n; v++) { + int d = 0; + for (int u = 0; u < n; u++) if (adj[(size_t)v * n + u]) nbr[d++] = u; + if (d < 2) { terms[v] = expr_new_integer(0); continue; } + long links = 0; + for (int i = 0; i < d; i++) + for (int j = i + 1; j < d; j++) + if (adj[(size_t)nbr[i] * n + nbr[j]]) links++; + terms[v] = ratio(2 * links, (long)d * (d - 1)); + } + free(nbr); free(adj); + + /* mean = (sum_v C_v) / n, reduced by the evaluator. */ + Expr* sum = expr_new_function(expr_new_symbol(SYM_Plus), terms, (size_t)n); + free(terms); + Expr* pa[2] = { expr_new_integer(n), expr_new_integer(-1) }; + Expr* inv = expr_new_function(expr_new_symbol(SYM_Power), pa, 2); + Expr* ta[2] = { sum, inv }; + return evaluate(expr_new_function(expr_new_symbol(SYM_Times), ta, 2)); +} diff --git a/src/graph/metrics.c b/src/graph/metrics.c new file mode 100644 index 00000000..4065c56e --- /dev/null +++ b/src/graph/metrics.c @@ -0,0 +1,155 @@ +/* metrics.c - distance metrics: VertexEccentricity, GraphDiameter, + * GraphRadius, GraphCenter. + * + * All derive from single-source BFS over the successor adjacency (out[]): for a + * directed graph this follows edge direction, for an undirected graph out[] is + * symmetric so it is ordinary shortest-path distance. The eccentricity of a + * vertex is the greatest distance from it to any other vertex, or Infinity if + * some vertex is unreachable (possible per-vertex for directed graphs). Then: + * diameter = max eccentricity (Infinity if any vertex has infinite ecc), + * radius = min finite eccentricity (Infinity if none is finite), + * center = the vertices attaining the radius. + * + * Complexity O(V*(V+E)) — a BFS per vertex over the on-demand integer adjacency. + * + * Memory (SPEC section 4): returns freshly-allocated results; frees res. Returns + * NULL (unevaluated) on a non-graph argument or an unknown vertex. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +/* Eccentricity of vertex `src`: max BFS distance to any vertex, or -1 if some + * vertex is unreachable. */ +static int eccentricity(const GraphAdj* a, int src, int* dist, int* q) { + int n = a->n; + for (int i = 0; i < n; i++) dist[i] = -1; + int head = 0, tail = 0, ecc = 0; + dist[src] = 0; q[tail++] = src; + while (head < tail) { + int u = q[head++]; + if (dist[u] > ecc) ecc = dist[u]; + for (int j = 0; j < a->outdeg[u]; j++) { + int w = a->out[u][j]; + if (dist[w] < 0) { dist[w] = dist[u] + 1; q[tail++] = w; } + } + } + for (int i = 0; i < n; i++) if (dist[i] < 0) return -1; /* unreachable */ + return ecc; +} + +/* Compute eccentricities for every vertex into ecc[] (-1 = infinite). Returns + * the built adjacency (caller frees) or NULL. Allocates/frees scratch itself. */ +static GraphAdj* all_eccentricities(const Expr* g, int** ecc_out) { + GraphAdj* a = graph_build_adj(g); + if (!a) return NULL; + int n = a->n; + int* ecc = malloc((size_t)(n > 0 ? n : 1) * sizeof(int)); + int* dist = malloc((size_t)(n > 0 ? n : 1) * sizeof(int)); + int* q = malloc((size_t)(n > 0 ? n : 1) * sizeof(int)); + if (!ecc || !dist || !q) { free(ecc); free(dist); free(q); graph_adj_free(a); return NULL; } + for (int i = 0; i < n; i++) ecc[i] = eccentricity(a, i, dist, q); + free(dist); free(q); + *ecc_out = ecc; + return a; +} + +/* VertexEccentricity[g] -> list; VertexEccentricity[g, v] -> one value. */ +Expr* builtin_vertex_eccentricity(Expr* res) { + size_t argc = res->data.function.arg_count; + if (argc != 1 && argc != 2) return NULL; + const Expr* g = res->data.function.args[0]; + int* ecc = NULL; + GraphAdj* a = all_eccentricities(g, &ecc); + if (!a) return NULL; + int n = a->n; + + Expr* out; + if (argc == 2) { + int v = graph_vertex_index(a->verts, res->data.function.args[1]); + if (v < 0) { free(ecc); graph_adj_free(a); return NULL; } + out = (ecc[v] < 0) ? expr_new_symbol(SYM_Infinity) : expr_new_integer(ecc[v]); + } else { + Expr** items = (n > 0) ? calloc((size_t)n, sizeof(Expr*)) : NULL; + for (int i = 0; i < n; i++) + items[i] = (ecc[i] < 0) ? expr_new_symbol(SYM_Infinity) : expr_new_integer(ecc[i]); + out = expr_new_function(expr_new_symbol(SYM_List), items, (size_t)n); + free(items); + } + free(ecc); graph_adj_free(a); + return out; +} + +Expr* builtin_graph_diameter(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + int* ecc = NULL; + GraphAdj* a = all_eccentricities(res->data.function.args[0], &ecc); + if (!a) return NULL; + int n = a->n, diam = 0, infinite = 0; + for (int i = 0; i < n; i++) { + if (ecc[i] < 0) { infinite = 1; break; } + if (ecc[i] > diam) diam = ecc[i]; + } + free(ecc); graph_adj_free(a); + return infinite ? expr_new_symbol(SYM_Infinity) : expr_new_integer(diam); +} + +Expr* builtin_graph_radius(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + int* ecc = NULL; + GraphAdj* a = all_eccentricities(res->data.function.args[0], &ecc); + if (!a) return NULL; + int n = a->n, radius = -1; + for (int i = 0; i < n; i++) + if (ecc[i] >= 0 && (radius < 0 || ecc[i] < radius)) radius = ecc[i]; + free(ecc); graph_adj_free(a); + return (radius < 0) ? expr_new_symbol(SYM_Infinity) : expr_new_integer(radius); +} + +/* GraphCenter[g] -> vertices attaining the (finite) radius; {} if the radius is + * Infinity (no vertex reaches all others). */ +Expr* builtin_graph_center(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + int* ecc = NULL; + GraphAdj* a = all_eccentricities(res->data.function.args[0], &ecc); + if (!a) return NULL; + int n = a->n, radius = -1; + for (int i = 0; i < n; i++) + if (ecc[i] >= 0 && (radius < 0 || ecc[i] < radius)) radius = ecc[i]; + + Expr** items = (n > 0) ? calloc((size_t)n, sizeof(Expr*)) : NULL; + size_t k = 0; + if (radius >= 0) + for (int i = 0; i < n; i++) + if (ecc[i] == radius) items[k++] = expr_copy(a->verts->data.function.args[i]); + Expr* out = expr_new_function(expr_new_symbol(SYM_List), items, k); + free(items); free(ecc); graph_adj_free(a); + return out; +} + +/* GraphPeriphery[g]: the vertices whose eccentricity equals the graph diameter. + * When some vertex has infinite eccentricity (the graph is not strongly + * connected), the diameter is Infinity and the periphery is exactly those + * infinite-eccentricity vertices. The dual of GraphCenter. */ +Expr* builtin_graph_periphery(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + int* ecc = NULL; + GraphAdj* a = all_eccentricities(res->data.function.args[0], &ecc); + if (!a) return NULL; + int n = a->n, infinite = 0, diam = -1; + for (int i = 0; i < n; i++) { + if (ecc[i] < 0) infinite = 1; + else if (ecc[i] > diam) diam = ecc[i]; + } + Expr** items = (n > 0) ? calloc((size_t)n, sizeof(Expr*)) : NULL; + size_t k = 0; + for (int i = 0; i < n; i++) { + int on_periphery = infinite ? (ecc[i] < 0) : (ecc[i] == diam && diam >= 0); + if (on_periphery) items[k++] = expr_copy(a->verts->data.function.args[i]); + } + Expr* out = expr_new_function(expr_new_symbol(SYM_List), items, k); + free(items); free(ecc); graph_adj_free(a); + return out; +} diff --git a/src/graph/mixedgraphq.c b/src/graph/mixedgraphq.c new file mode 100644 index 00000000..f3601323 --- /dev/null +++ b/src/graph/mixedgraphq.c @@ -0,0 +1,26 @@ +/* mixedgraphq.c - MixedGraphQ[g]: True iff g has at least one directed edge and + * at least one undirected edge (a mixed graph). A purely directed, purely + * undirected, or edgeless graph is not mixed. O(E). + * + * Memory (SPEC section 4): returns True/False; frees res. NULL on a non-graph. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" + +Expr* builtin_mixed_graph_q(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + + const Expr* edges = g->data.function.args[1]; + int has_dir = 0, has_undir = 0; + for (size_t i = 0; i < edges->data.function.arg_count; i++) { + const char* k = graph_edge_kind(edges->data.function.args[i]); + if (k == SYM_DirectedEdge) has_dir = 1; + else if (k == SYM_UndirectedEdge) has_undir = 1; + if (has_dir && has_undir) break; + } + return expr_new_symbol((has_dir && has_undir) ? SYM_True : SYM_False); +} diff --git a/src/graph/neighborhoodgraph.c b/src/graph/neighborhoodgraph.c new file mode 100644 index 00000000..88439ce0 --- /dev/null +++ b/src/graph/neighborhoodgraph.c @@ -0,0 +1,81 @@ +/* neighborhoodgraph.c - NeighborhoodGraph[g, v] / NeighborhoodGraph[g, v, k]: the + * subgraph of g induced by v together with every vertex within graph distance k + * of v (k defaults to 1). The induced subgraph keeps all edges of g whose both + * endpoints lie in that neighbourhood. + * + * A depth-limited BFS from v over the successor adjacency (out[]) collects the + * neighbourhood -- direction-aware, matching GraphDistance -- then the edges of g + * between kept vertices are retained (kinds preserved). O(V + E). k must be a + * non-negative integer; k = 0 gives the single vertex v. + * + * Memory (SPEC section 4): returns a freshly-built Graph; frees res. NULL on a + * non-graph, an unknown vertex, or a negative/non-integer k. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +Expr* builtin_neighborhood_graph(Expr* res) { + size_t argc = res->data.function.arg_count; + if (argc != 2 && argc != 3) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + + long k = 1; + if (argc == 3) { + const Expr* ke = res->data.function.args[2]; + if (ke->type != EXPR_INTEGER || ke->data.integer < 0) return NULL; + k = (long)ke->data.integer; + } + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + int n = (int)verts->data.function.arg_count; + int src = graph_vertex_index(verts, res->data.function.args[1]); + if (src < 0) return NULL; /* v is not a vertex */ + + GraphAdj* a = graph_build_adj(g); + if (!a) return NULL; + + /* Depth-limited BFS from src: keep[i] set for dist(src, i) <= k. */ + char* keep = calloc((size_t)n, 1); + int* dist = malloc((size_t)n * sizeof(int)); + int* q = malloc((size_t)n * sizeof(int)); + for (int i = 0; i < n; i++) dist[i] = -1; + int head = 0, tail = 0; + dist[src] = 0; keep[src] = 1; q[tail++] = src; + while (head < tail) { + int v = q[head++]; + if (dist[v] >= (int)k) continue; + for (int j = 0; j < a->outdeg[v]; j++) { + int u = a->out[v][j]; + if (dist[u] < 0) { dist[u] = dist[v] + 1; keep[u] = 1; q[tail++] = u; } + } + } + free(dist); free(q); + graph_adj_free(a); + + /* Induced subgraph on the kept vertices. */ + Expr** vc = malloc((size_t)(n > 0 ? n : 1) * sizeof(Expr*)); + size_t nv = 0; + for (int i = 0; i < n; i++) if (keep[i]) vc[nv++] = expr_copy(verts->data.function.args[i]); + + size_t m = edges->data.function.arg_count; + Expr** ec = (m > 0) ? malloc(m * sizeof(Expr*)) : NULL; + size_t me = 0; + for (size_t e = 0; e < m; e++) { + const Expr* ed = edges->data.function.args[e]; + int ia = graph_vertex_index(verts, ed->data.function.args[0]); + int ib = graph_vertex_index(verts, ed->data.function.args[1]); + if (ia >= 0 && ib >= 0 && keep[ia] && keep[ib]) ec[me++] = expr_copy((Expr*)ed); + } + free(keep); + + Expr* vlist = expr_new_function(expr_new_symbol(SYM_List), vc, nv); + Expr* elist = expr_new_function(expr_new_symbol(SYM_List), ec, me); + free(vc); free(ec); + Expr* gargs[2] = { vlist, elist }; + return expr_new_function(expr_new_symbol(SYM_Graph), gargs, 2); +} diff --git a/src/graph/pagerank.c b/src/graph/pagerank.c new file mode 100644 index 00000000..7538f249 --- /dev/null +++ b/src/graph/pagerank.c @@ -0,0 +1,74 @@ +/* pagerank.c - PageRankCentrality[g]: the PageRank of each vertex -- the + * stationary distribution of a random surfer that follows out-edges with + * probability d = 17/20 and teleports uniformly with probability 1 - d. + * + * Instead of iterating to a floating-point fixed point, we solve the defining + * linear system exactly and return rationals. PageRank satisfies + * + * pi = (1-d)/n * 1 + d * M pi, i.e. (I - d M) pi = (1-d)/n * 1, + * + * where M is the column-stochastic transition matrix: M[v][u] = 1/outdeg(u) if + * u->v, and 1/n for a dangling u (no out-edges, teleport everywhere). Because M + * is column-stochastic and d < 1, (I - dM) is invertible and the solution sums + * to 1. We assemble the rational matrix and right-hand side and hand them to the + * exact LinearSolve, so the whole result is a reduced-rational vector. Edge + * direction is followed (out[] is symmetric for undirected graphs). O(V^3) from + * the solve. + * + * Memory (SPEC section 4): returns a fresh List; frees res. NULL on a non-graph. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include "eval.h" +#include + +/* Exact num/den as a reduced value (integer or Rational). den > 0 required. */ +static Expr* ratio(long num, long den) { + Expr* pa[2] = { expr_new_integer(den), expr_new_integer(-1) }; + Expr* inv = expr_new_function(expr_new_symbol(SYM_Power), pa, 2); + Expr* ta[2] = { expr_new_integer(num), inv }; + return evaluate(expr_new_function(expr_new_symbol(SYM_Times), ta, 2)); +} + +Expr* builtin_pagerank_centrality(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + GraphAdj* a = graph_build_adj(res->data.function.args[0]); + if (!a) return NULL; + int n = a->n; + if (n == 0) { graph_adj_free(a); return expr_new_function(expr_new_symbol(SYM_List), NULL, 0); } + + /* Successor adjacency as a boolean matrix. */ + char* out = calloc((size_t)n * (size_t)n, 1); + for (int u = 0; u < n; u++) + for (int j = 0; j < a->outdeg[u]; j++) out[(size_t)u * n + a->out[u][j]] = 1; + + /* Damping d = 17/20; teleport (1-d) = 3/20. */ + Expr** rows = calloc((size_t)n, sizeof(Expr*)); + Expr** bvec = calloc((size_t)n, sizeof(Expr*)); + for (int v = 0; v < n; v++) { + Expr** row = calloc((size_t)n, sizeof(Expr*)); + for (int u = 0; u < n; u++) { + long mnum, mden; + if (a->outdeg[u] == 0) { mnum = 1; mden = n; } /* dangling */ + else { mnum = out[(size_t)u * n + v] ? 1 : 0; mden = a->outdeg[u]; } + /* A[v][u] = [v==u] - (17/20)*(mnum/mden) + * = ([v==u]*20*mden - 17*mnum) / (20*mden) */ + long num = (long)(v == u) * 20 * mden - 17 * mnum; + row[u] = ratio(num, 20 * mden); + } + rows[v] = expr_new_function(expr_new_symbol(SYM_List), row, (size_t)n); + free(row); + bvec[v] = ratio(3, 20L * n); /* (1-d)/n */ + } + free(out); + graph_adj_free(a); + + Expr* matrix = expr_new_function(expr_new_symbol(SYM_List), rows, (size_t)n); + Expr* vec = expr_new_function(expr_new_symbol(SYM_List), bvec, (size_t)n); + free(rows); free(bvec); + Expr* args[2] = { matrix, vec }; + Expr* solve = expr_new_function(expr_new_symbol("LinearSolve"), args, 2); + return evaluate(solve); +} diff --git a/src/graph/pathgraphq.c b/src/graph/pathgraphq.c new file mode 100644 index 00000000..0c9d8487 --- /dev/null +++ b/src/graph/pathgraphq.c @@ -0,0 +1,66 @@ +/* pathgraphq.c - PathGraphQ[g]: True iff g is a path graph -- the vertices can be + * arranged in a line so that consecutive ones are joined and no others are. + * + * A path is exactly a tree whose maximum degree is at most 2, so the test is: + * connected (on the underlying undirected graph) with n-1 distinct edges (tree) + * and every vertex of undirected degree <= 2. Edge direction is ignored, and + * a->b together with b->a counts as one undirected edge. O(V^2) via the boolean + * adjacency. A single vertex is a (trivial) path; the empty graph and any + * disconnected, cyclic, or branching graph is not. + * + * Memory (SPEC section 4): returns True/False; frees res. NULL on a non-graph. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +Expr* builtin_path_graph_q(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + int n = (int)verts->data.function.arg_count; + size_t ne = edges->data.function.arg_count; + if (n == 0) return expr_new_symbol(SYM_False); + + char* adj = calloc((size_t)n * (size_t)n, 1); + int* deg = calloc((size_t)n, sizeof(int)); + long m = 0; + for (size_t k = 0; k < ne; k++) { + const Expr* e = edges->data.function.args[k]; + int ia = graph_vertex_index(verts, e->data.function.args[0]); + int ib = graph_vertex_index(verts, e->data.function.args[1]); + if (ia < 0 || ib < 0 || ia == ib) continue; + if (!adj[(size_t)ia * n + ib]) { + adj[(size_t)ia * n + ib] = adj[(size_t)ib * n + ia] = 1; + deg[ia]++; deg[ib]++; m++; + } + } + + /* Max degree <= 2 (a path never branches). */ + int ok = 1; + for (int i = 0; i < n && ok; i++) if (deg[i] > 2) ok = 0; + + /* Connected via BFS over the undirected adjacency. */ + int reached = 0; + if (ok) { + char* seen = calloc((size_t)n, 1); + int* q = malloc((size_t)n * sizeof(int)); + int head = 0, tail = 0; + seen[0] = 1; q[tail++] = 0; + while (head < tail) { + int v = q[head++]; reached++; + for (int u = 0; u < n; u++) + if (adj[(size_t)v * n + u] && !seen[u]) { seen[u] = 1; q[tail++] = u; } + } + free(seen); free(q); + } + free(adj); free(deg); + + int is_path = ok && (reached == n) && (m == (long)n - 1); /* tree + deg<=2 */ + return expr_new_symbol(is_path ? SYM_True : SYM_False); +} diff --git a/src/graph/prismgraph.c b/src/graph/prismgraph.c new file mode 100644 index 00000000..09adbed3 --- /dev/null +++ b/src/graph/prismgraph.c @@ -0,0 +1,47 @@ +/* prismgraph.c - PrismGraph[n]: the n-gonal prism (a.k.a. circular ladder), two + * concentric n-cycles joined rung-by-rung -- the Cartesian product C_n [] K_2. + * + * Vertices are the outer cycle 1..n and the inner cycle n+1..2n. Edges are the + * outer cycle o_i ~ o_{i+1}, the inner cycle c_i ~ c_{i+1}, and the rungs + * o_i ~ c_i (indices mod n). 3-regular with 3n edges. O(n). PrismGraph[3] is the + * triangular prism, PrismGraph[4] the cube. (Isomorphic to + * GeneralizedPetersenGraph[n, 1], provided under its own common name.) + * + * n >= 3 must be an integer. Memory (SPEC section 4): returns a freshly-built + * Graph; frees res. NULL otherwise. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +Expr* builtin_prism_graph(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* ne = res->data.function.args[0]; + if (ne->type != EXPR_INTEGER) return NULL; + long n = (long)ne->data.integer; + if (n < 3) return NULL; + + long V = 2 * n; + Expr** vc = malloc((size_t)V * sizeof(Expr*)); + for (long i = 0; i < V; i++) vc[i] = expr_new_integer(i + 1); + + Expr** ec = malloc((size_t)(3 * n) * sizeof(Expr*)); + size_t me = 0; + #define ADD(a,b) do { Expr* _e[2] = { expr_new_integer(a), expr_new_integer(b) }; \ + ec[me++] = expr_new_function(expr_new_symbol(SYM_UndirectedEdge), _e, 2); } while (0) + for (long i = 0; i < n; i++) { + long o = i + 1, o1 = (i + 1) % n + 1, c = n + i + 1, c1 = n + (i + 1) % n + 1; + ADD(o, o1); /* outer cycle */ + ADD(c, c1); /* inner cycle */ + ADD(o, c); /* rung */ + } + #undef ADD + + Expr* vlist = expr_new_function(expr_new_symbol(SYM_List), vc, (size_t)V); + Expr* elist = expr_new_function(expr_new_symbol(SYM_List), ec, me); + free(vc); free(ec); + Expr* gargs[2] = { vlist, elist }; + return expr_new_function(expr_new_symbol(SYM_Graph), gargs, 2); +} diff --git a/src/graph/reciprocity.c b/src/graph/reciprocity.c new file mode 100644 index 00000000..eac8ee4b --- /dev/null +++ b/src/graph/reciprocity.c @@ -0,0 +1,64 @@ +/* reciprocity.c - GraphReciprocity[g]: the fraction of arcs whose reverse is + * also present, an exact rational in [0, 1]. It measures how mutual a directed + * graph's connections are; an undirected graph is fully reciprocal (1). + * + * Model everything as a set of directed arcs on an n x n boolean matrix: a + * DirectedEdge a->b sets one arc, an UndirectedEdge {a,b} sets both a->b and + * b->a (an undirected edge is inherently mutual). Then + * + * reciprocity = #{arcs (a,b) with (b,a) also present} / #arcs. + * + * For a purely directed graph this is the usual "fraction of edges that are + * reciprocated"; for an undirected graph every arc has its reverse, giving 1. + * With no edges the ratio is undefined, reported as 0 by convention. O(V^2). + * + * Memory (SPEC section 4): returns a fresh number; frees res. NULL on a + * non-graph argument. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include "eval.h" +#include + +/* Exact num/den as a reduced value (integer or Rational). den > 0 required. */ +static Expr* ratio(long num, long den) { + Expr* pa[2] = { expr_new_integer(den), expr_new_integer(-1) }; + Expr* inv = expr_new_function(expr_new_symbol(SYM_Power), pa, 2); + Expr* ta[2] = { expr_new_integer(num), inv }; + return evaluate(expr_new_function(expr_new_symbol(SYM_Times), ta, 2)); +} + +Expr* builtin_graph_reciprocity(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + int n = (int)verts->data.function.arg_count; + size_t ne = edges->data.function.arg_count; + + char* arc = (n > 0) ? calloc((size_t)n * (size_t)n, 1) : NULL; + for (size_t k = 0; k < ne; k++) { + const Expr* ed = edges->data.function.args[k]; + int ia = graph_vertex_index(verts, ed->data.function.args[0]); + int ib = graph_vertex_index(verts, ed->data.function.args[1]); + if (ia < 0 || ib < 0 || ia == ib) continue; + arc[(size_t)ia * n + ib] = 1; + if (graph_edge_kind(ed) == SYM_UndirectedEdge) arc[(size_t)ib * n + ia] = 1; + } + + long total = 0, recip = 0; + for (int i = 0; i < n; i++) + for (int j = 0; j < n; j++) + if (arc[(size_t)i * n + j]) { + total++; + if (arc[(size_t)j * n + i]) recip++; + } + free(arc); + + if (total == 0) return expr_new_integer(0); /* no arcs -> undefined, 0 */ + return ratio(recip, total); +} diff --git a/src/graph/regulargraphq.c b/src/graph/regulargraphq.c new file mode 100644 index 00000000..eaed84bb --- /dev/null +++ b/src/graph/regulargraphq.c @@ -0,0 +1,28 @@ +/* regulargraphq.c - RegularGraphQ[g]: True iff g is regular -- every vertex has + * the same degree. For a directed graph this means all out-degrees are equal + * and all in-degrees are equal (an undirected graph has out == in per vertex, + * so it reduces to "all degrees equal"). + * + * One pass over the adjacency degree arrays, O(V). A graph with 0 or 1 vertex is + * vacuously regular. + * + * Memory (SPEC section 4): returns True/False; frees res. NULL on a non-graph. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +Expr* builtin_regular_graph_q(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + GraphAdj* a = graph_build_adj(res->data.function.args[0]); + if (!a) return NULL; + int n = a->n; + + int regular = 1; + for (int i = 1; i < n; i++) + if (a->outdeg[i] != a->outdeg[0] || a->indeg[i] != a->indeg[0]) { regular = 0; break; } + graph_adj_free(a); + return expr_new_symbol(regular ? SYM_True : SYM_False); +} diff --git a/src/graph/render3d.c b/src/graph/render3d.c new file mode 100644 index 00000000..109372cc --- /dev/null +++ b/src/graph/render3d.c @@ -0,0 +1,110 @@ +/* render3d.c - render a Graph3D as a Graphics3D[...] node-link diagram. + * + * Mirrors graph_render() (src/graph/graphplot.c) but in three dimensions: edges + * become 3D Lines and vertices a Point set, each preceded by an RGBColor + * directive. The notebook serializer (src/graphics/graphics_json.c) turns a + * Graphics3D into Plotly scatter3d traces; the REPL auto-displays a bare + * Graph3D the same way it does Graph / Graphics. + * + * Coordinates come from graph_compute_layout3d() (src/graph/layout.c). Vertex + * labels are omitted in 3D (Plotly 3D annotations are awkward and the Wolfram + * default 3D graph shows none). + * + * Memory (SPEC section 4): returns a fresh Graphics3D tree; read-only over its + * arguments. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +static const double D3_VERTEX[3] = { 0.24, 0.52, 0.90 }; /* blue */ +static const double D3_EDGE[3] = { 0.55, 0.60, 0.72 }; /* slate */ +static const double D3_ACCENT[3] = { 0.95, 0.45, 0.10 }; /* highlight orange */ +static const double D3_DIM[3] = { 0.72, 0.75, 0.82 }; /* dimmed grey */ + +static Expr* point3(double x, double y, double z) { + Expr* xyz[3] = { expr_new_real(x), expr_new_real(y), expr_new_real(z) }; + return expr_new_function(expr_new_symbol(SYM_List), xyz, 3); +} + +static Expr* rgb3(const double c[3]) { + Expr* a[3] = { expr_new_real(c[0]), expr_new_real(c[1]), expr_new_real(c[2]) }; + return expr_new_function(expr_new_symbol(SYM_RGBColor), a, 3); +} + +Expr* graph_render3d(const Expr* g, const GraphStyle* st) { + if (!graph_is_valid_head(g, SYM_Graph3D)) return NULL; + GraphStyle defaults = {0}; + defaults.show_labels = 0; + if (!st) st = &defaults; + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + int n = (int)verts->data.function.arg_count; + size_t ne = edges->data.function.arg_count; + + double* x = (n > 0) ? calloc((size_t)n, sizeof(double)) : NULL; + double* y = (n > 0) ? calloc((size_t)n, sizeof(double)) : NULL; + double* z = (n > 0) ? calloc((size_t)n, sizeof(double)) : NULL; + if (n > 0 && (!x || !y || !z)) { free(x); free(y); free(z); return NULL; } + graph_compute_layout3d(g, st->layout, x, y, z); + + /* (color + line) per edge, then up to two vertex Point sets (normal + + * highlighted), each with a color directive. */ + size_t cap = ne * 2 + 4; + Expr** prims = (cap > 0) ? calloc(cap, sizeof(Expr*)) : NULL; + size_t p = 0; + + for (size_t kk = 0; kk < ne; kk++) { + const Expr* e = edges->data.function.args[kk]; + int iu = graph_vertex_index(verts, e->data.function.args[0]); + int iv = graph_vertex_index(verts, e->data.function.args[1]); + if (iu < 0 || iv < 0) continue; + const double* ec = st->hi_edge ? (st->hi_edge[kk] ? D3_ACCENT : D3_DIM) : D3_EDGE; + prims[p++] = rgb3(ec); + Expr* pts[2] = { point3(x[iu], y[iu], z[iu]), point3(x[iv], y[iv], z[iv]) }; + Expr* seg = expr_new_function(expr_new_symbol(SYM_List), pts, 2); + Expr* la[1] = { seg }; + prims[p++] = expr_new_function(expr_new_symbol(SYM_Line), la, 1); + } + + /* Vertices: one Point set for the normal color, one for highlighted (if a + * highlight mask is present). */ + for (int pass = 0; pass < 2; pass++) { + int want_hi = (pass == 1); + if (want_hi && !st->hi_vert) break; + int cnt = 0; + for (int i = 0; i < n; i++) { + int hi = st->hi_vert ? st->hi_vert[i] : 0; + if (st->hi_vert && (hi != want_hi)) continue; + cnt++; + } + if (cnt == 0) continue; + Expr** coords = calloc((size_t)cnt, sizeof(Expr*)); + int ci = 0; + for (int i = 0; i < n; i++) { + int hi = st->hi_vert ? st->hi_vert[i] : 0; + if (st->hi_vert && (hi != want_hi)) continue; + coords[ci++] = point3(x[i], y[i], z[i]); + } + const double* vc = st->hi_vert ? (want_hi ? D3_ACCENT : D3_DIM) : D3_VERTEX; + prims[p++] = rgb3(vc); + Expr* plist = expr_new_function(expr_new_symbol(SYM_List), coords, (size_t)cnt); + free(coords); + Expr* pa[1] = { plist }; + prims[p++] = expr_new_function(expr_new_symbol(SYM_Point), pa, 1); + if (!st->hi_vert) break; /* single uniform set */ + } + + free(x); free(y); free(z); + Expr* prim_list = expr_new_function(expr_new_symbol(SYM_List), prims, p); + free(prims); + Expr* ga[1] = { prim_list }; + return expr_new_function(expr_new_symbol(SYM_Graphics3D), ga, 1); +} + +Expr* graph_default_graphics3d(const Expr* g) { + return graph_render3d(g, NULL); +} \ No newline at end of file diff --git a/src/graph/shortestpath.c b/src/graph/shortestpath.c new file mode 100644 index 00000000..f6df30de --- /dev/null +++ b/src/graph/shortestpath.c @@ -0,0 +1,86 @@ +/* shortestpath.c - FindShortestPath[g,s,t] and GraphDistance[g,s,t]. + * + * Unweighted breadth-first search over the successor adjacency (out[]): for a + * directed graph this follows edge direction; for an undirected graph out[] is + * symmetric, so it is an ordinary shortest path. Wolfram's naming split is + * kept: FindShortestPath returns the vertex path, GraphDistance the length. + * + * Unreachable target: FindShortestPath -> {} (empty list), GraphDistance -> + * Infinity. + * + * Memory (SPEC section 4): returns freshly-allocated results; frees res. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +/* BFS from src over out[]; fills parent[] (-1 = root/unvisited) and dist[] + * (-1 = unreached). Caller allocates parent/dist of length a->n. */ +static void bfs(const GraphAdj* a, int src, int* parent, int* dist) { + for (int i = 0; i < a->n; i++) { parent[i] = -1; dist[i] = -1; } + int* q = calloc((size_t)(a->n > 0 ? a->n : 1), sizeof(int)); + int head = 0, tail = 0; + dist[src] = 0; q[tail++] = src; + while (head < tail) { + int u = q[head++]; + for (int j = 0; j < a->outdeg[u]; j++) { + int w = a->out[u][j]; + if (dist[w] < 0) { dist[w] = dist[u] + 1; parent[w] = u; q[tail++] = w; } + } + } + free(q); +} + +/* Resolve g, s, t to a GraphAdj and endpoint indices. Returns adj (caller frees) + * or NULL; on success sets *is,*it. */ +static GraphAdj* resolve(Expr* res, int* is, int* it) { + if (res->data.function.arg_count != 3) return NULL; + const Expr* g = res->data.function.args[0]; + GraphAdj* a = graph_build_adj(g); + if (!a) return NULL; + *is = graph_vertex_index(a->verts, res->data.function.args[1]); + *it = graph_vertex_index(a->verts, res->data.function.args[2]); + if (*is < 0 || *it < 0) { graph_adj_free(a); return NULL; } + return a; +} + +Expr* builtin_find_shortest_path(Expr* res) { + int is, it; + GraphAdj* a = resolve(res, &is, &it); + if (!a) return NULL; + + int* parent = calloc((size_t)a->n, sizeof(int)); + int* dist = calloc((size_t)a->n, sizeof(int)); + bfs(a, is, parent, dist); + + Expr* out; + if (dist[it] < 0) { + out = expr_new_function(expr_new_symbol(SYM_List), NULL, 0); /* {} */ + } else { + int len = dist[it] + 1; + Expr** path = calloc((size_t)len, sizeof(Expr*)); + int v = it; + for (int k = len - 1; k >= 0; k--) { path[k] = expr_copy(a->verts->data.function.args[v]); v = parent[v]; } + out = expr_new_function(expr_new_symbol(SYM_List), path, (size_t)len); + free(path); + } + free(parent); free(dist); graph_adj_free(a); + return out; +} + +Expr* builtin_graph_distance(Expr* res) { + int is, it; + GraphAdj* a = resolve(res, &is, &it); + if (!a) return NULL; + + int* parent = calloc((size_t)a->n, sizeof(int)); + int* dist = calloc((size_t)a->n, sizeof(int)); + bfs(a, is, parent, dist); + + Expr* out = (dist[it] < 0) ? expr_new_symbol(SYM_Infinity) + : expr_new_integer(dist[it]); + free(parent); free(dist); graph_adj_free(a); + return out; +} diff --git a/src/graph/spanningtree.c b/src/graph/spanningtree.c new file mode 100644 index 00000000..6a034767 --- /dev/null +++ b/src/graph/spanningtree.c @@ -0,0 +1,75 @@ +/* spanningtree.c - FindSpanningTree[g]. + * + * A BFS spanning forest of the underlying undirected graph: for each component, + * the tree edges chosen by BFS are collected in their original form (preserving + * DirectedEdge/UndirectedEdge and orientation). Returns Graph[verts, treeEdges]; + * for a connected graph the tree has VertexCount - 1 edges. + * + * Memory (SPEC section 4): returns a freshly-allocated Graph; frees res. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +/* Copy the original edge of g connecting vertex indices iu and iv (either + * orientation / kind), or NULL if none. */ +static Expr* original_edge_copy(const Expr* g, int iu, int iv) { + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + for (size_t k = 0; k < edges->data.function.arg_count; k++) { + const Expr* e = edges->data.function.args[k]; + int a = graph_vertex_index(verts, e->data.function.args[0]); + int b = graph_vertex_index(verts, e->data.function.args[1]); + if ((a == iu && b == iv) || (a == iv && b == iu)) return expr_copy((Expr*)e); + } + return NULL; +} + +Expr* builtin_find_spanning_tree(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + GraphAdj* a = graph_build_adj(g); + if (!a) return NULL; + int n = a->n; + + char* seen = calloc((size_t)(n > 0 ? n : 1), sizeof(char)); + int* q = calloc((size_t)(n > 0 ? n : 1), sizeof(int)); + Expr** tree = (n > 0) ? calloc((size_t)n, sizeof(Expr*)) : NULL; /* <= n-1 edges */ + size_t te = 0; + + for (int s = 0; s < n; s++) { + if (seen[s]) continue; + int head = 0, tail = 0; + seen[s] = 1; q[tail++] = s; + while (head < tail) { + int u = q[head++]; + /* Underlying undirected neighbors: out then in. */ + for (int pass = 0; pass < 2; pass++) { + int deg = pass ? a->indeg[u] : a->outdeg[u]; + int* nb = pass ? a->in[u] : a->out[u]; + for (int j = 0; j < deg; j++) { + int w = nb[j]; + if (seen[w]) continue; + seen[w] = 1; q[tail++] = w; + Expr* e = original_edge_copy(g, u, w); + if (e) tree[te++] = e; + } + } + } + } + + Expr* elist = expr_new_function(expr_new_symbol(SYM_List), tree, te); + free(tree); + /* Fresh copy of the vertex list. */ + size_t nv = (size_t)n; + Expr** vcopy = (nv > 0) ? calloc(nv, sizeof(Expr*)) : NULL; + for (size_t i = 0; i < nv; i++) vcopy[i] = expr_copy(a->verts->data.function.args[i]); + Expr* vlist = expr_new_function(expr_new_symbol(SYM_List), vcopy, nv); + free(vcopy); + + free(seen); free(q); graph_adj_free(a); + Expr* gargs[2] = { vlist, elist }; + return expr_new_function(expr_new_symbol(SYM_Graph), gargs, 2); +} diff --git a/src/graph/stronglyconnectedq.c b/src/graph/stronglyconnectedq.c new file mode 100644 index 00000000..06e197ef --- /dev/null +++ b/src/graph/stronglyconnectedq.c @@ -0,0 +1,52 @@ +/* stronglyconnectedq.c - StronglyConnectedGraphQ[g]: True iff g is strongly + * connected -- every vertex is reachable from every other following edge + * directions. (For an undirected graph this coincides with ordinary + * connectivity, since out- and in-neighbourhoods are the same.) + * + * A graph is strongly connected iff, from any single vertex, every other vertex + * is reachable forward (following out-edges) AND that vertex is reachable from + * every other, i.e. every vertex reaches it -- which is a forward reach on the + * reverse graph (following in-edges). So two BFS from vertex 0, one over out[] + * and one over in[], each reaching all n vertices, settle it in O(V+E). A single + * vertex is trivially strongly connected; the empty graph is not. + * + * Memory (SPEC section 4): returns True/False; frees res. NULL on a non-graph. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +/* Count vertices reachable from src using successor lists nbr/deg. */ +static int reach_count(int n, int src, int* const* nbr, const int* deg, int* q, char* seen) { + for (int i = 0; i < n; i++) seen[i] = 0; + int head = 0, tail = 0, cnt = 0; + seen[src] = 1; q[tail++] = src; + while (head < tail) { + int v = q[head++]; cnt++; + for (int j = 0; j < deg[v]; j++) { + int u = nbr[v][j]; + if (!seen[u]) { seen[u] = 1; q[tail++] = u; } + } + } + return cnt; +} + +Expr* builtin_strongly_connected_graph_q(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + GraphAdj* a = graph_build_adj(res->data.function.args[0]); + if (!a) return NULL; + int n = a->n; + if (n == 0) { graph_adj_free(a); return expr_new_symbol(SYM_False); } + + int* q = malloc((size_t)n * sizeof(int)); + char* seen = malloc((size_t)n); + int fwd = reach_count(n, 0, a->out, a->outdeg, q, seen); /* 0 reaches all? */ + int bwd = reach_count(n, 0, a->in, a->indeg, q, seen); /* all reach 0? */ + free(q); free(seen); + graph_adj_free(a); + + int strong = (fwd == n && bwd == n); + return expr_new_symbol(strong ? SYM_True : SYM_False); +} diff --git a/src/graph/subgraph.c b/src/graph/subgraph.c new file mode 100644 index 00000000..6f2e11a9 --- /dev/null +++ b/src/graph/subgraph.c @@ -0,0 +1,57 @@ +/* subgraph.c - Subgraph[g, {v1, v2, ...}]: the subgraph of g induced by the given + * vertices -- those vertices (in the order listed, de-duplicated, restricted to + * vertices actually in g) together with exactly the edges of g whose BOTH + * endpoints are among them. + * + * O(V_sub + E) with linear membership tests (small-graph scale). Edge kinds are + * preserved. Subgraph[K4, {1,2,3}] is K3; Subgraph[CycleGraph[5], {1,2,3}] is the + * path 1-2-3; an empty vertex list gives the empty graph. + * + * Memory (SPEC section 4): returns a freshly-built Graph; frees res. NULL unless + * g is a valid graph and the second argument is a List. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +Expr* builtin_subgraph(Expr* res) { + if (res->data.function.arg_count != 2) return NULL; + const Expr* g = res->data.function.args[0]; + const Expr* sel = res->data.function.args[1]; + if (!graph_is_valid(g) || !graph_is_list(sel)) return NULL; + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + int n = (int)verts->data.function.arg_count; + size_t m = edges->data.function.arg_count; + + /* keep[i] = 1 if vertex i of g is selected. Also record selection order. */ + char* keep = (n > 0) ? calloc((size_t)n, 1) : NULL; + Expr** vc = malloc((sel->data.function.arg_count > 0 ? sel->data.function.arg_count : 1) * sizeof(Expr*)); + size_t nv = 0; + for (size_t i = 0; i < sel->data.function.arg_count; i++) { + int vi = graph_vertex_index(verts, sel->data.function.args[i]); + if (vi < 0 || keep[vi]) continue; /* not in g, or duplicate */ + keep[vi] = 1; + vc[nv++] = expr_copy(verts->data.function.args[vi]); + } + + /* Keep edges with both endpoints selected. */ + Expr** ec = (m > 0) ? malloc(m * sizeof(Expr*)) : NULL; + size_t me = 0; + for (size_t k = 0; k < m; k++) { + const Expr* e = edges->data.function.args[k]; + int a = graph_vertex_index(verts, e->data.function.args[0]); + int b = graph_vertex_index(verts, e->data.function.args[1]); + if (a >= 0 && b >= 0 && keep[a] && keep[b]) ec[me++] = expr_copy((Expr*)e); + } + free(keep); + + Expr* vlist = expr_new_function(expr_new_symbol(SYM_List), vc, nv); + Expr* elist = expr_new_function(expr_new_symbol(SYM_List), ec, me); + free(vc); free(ec); + Expr* gargs[2] = { vlist, elist }; + return expr_new_function(expr_new_symbol(SYM_Graph), gargs, 2); +} diff --git a/src/graph/sunletgraph.c b/src/graph/sunletgraph.c new file mode 100644 index 00000000..538d3984 --- /dev/null +++ b/src/graph/sunletgraph.c @@ -0,0 +1,44 @@ +/* sunletgraph.c - SunletGraph[n]: the n-sunlet graph, a cycle C_n with one + * pendant vertex attached to each cycle vertex ("rays of the sun"). + * + * Vertices are the cycle 1..n and the pendants n+1..2n; edges are the cycle + * i ~ i+1 (mod n) and the pendant edges i ~ n+i. So 2n vertices and 2n edges: + * each cycle vertex has degree 3, each pendant degree 1. O(n). Also called the + * sun/corona C_n o K_1. + * + * n >= 3 must be an integer. Memory (SPEC section 4): returns a freshly-built + * Graph; frees res. NULL otherwise. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +Expr* builtin_sunlet_graph(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* ne = res->data.function.args[0]; + if (ne->type != EXPR_INTEGER) return NULL; + long n = (long)ne->data.integer; + if (n < 3) return NULL; + + long V = 2 * n; + Expr** vc = malloc((size_t)V * sizeof(Expr*)); + for (long i = 0; i < V; i++) vc[i] = expr_new_integer(i + 1); + + Expr** ec = malloc((size_t)(2 * n) * sizeof(Expr*)); + size_t me = 0; + #define ADD(a,b) do { Expr* _e[2] = { expr_new_integer(a), expr_new_integer(b) }; \ + ec[me++] = expr_new_function(expr_new_symbol(SYM_UndirectedEdge), _e, 2); } while (0) + for (long i = 0; i < n; i++) { + ADD(i + 1, (i + 1) % n + 1); /* cycle edge */ + ADD(i + 1, n + i + 1); /* pendant edge */ + } + #undef ADD + + Expr* vlist = expr_new_function(expr_new_symbol(SYM_List), vc, (size_t)V); + Expr* elist = expr_new_function(expr_new_symbol(SYM_List), ec, me); + free(vc); free(ec); + Expr* gargs[2] = { vlist, elist }; + return expr_new_function(expr_new_symbol(SYM_Graph), gargs, 2); +} diff --git a/src/graph/transitiveclosure.c b/src/graph/transitiveclosure.c new file mode 100644 index 00000000..26ea0c89 --- /dev/null +++ b/src/graph/transitiveclosure.c @@ -0,0 +1,99 @@ +/* transitiveclosure.c - TransitiveClosure[g]. + * + * directed g -> directed closure: an edge u->v for every v (!= u) reachable + * from u by a directed path. + * undirected g -> each connected component becomes a clique (u<->v for every + * pair in the same component). + * + * Directed: a BFS per source over out[] marks its reachable set (O(V*(V+E))). + * Undirected: one component labeling, then all within-component pairs. + * + * Memory (SPEC section 4): returns the canonical Graph directly; frees res. + * NULL on a non-graph argument. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +Expr* builtin_transitive_closure(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + int n = (int)verts->data.function.arg_count; + size_t ne = edges->data.function.arg_count; + int directed = (ne > 0); + for (size_t i = 0; i < ne; i++) + if (graph_edge_kind(edges->data.function.args[i]) != SYM_DirectedEdge) { directed = 0; break; } + + GraphAdj* a = graph_build_adj(g); + if (!a) return NULL; + + /* Worst case n(n-1) directed edges (n(n-1)/2 undirected). */ + size_t cap = (size_t)n * (size_t)(n > 0 ? n - 1 : 0); + Expr** cedges = (cap > 0) ? calloc(cap, sizeof(Expr*)) : NULL; + size_t m = 0; + int* dist = (n > 0) ? malloc((size_t)n * sizeof(int)) : NULL; + int* q = (n > 0) ? malloc((size_t)n * sizeof(int)) : NULL; + + if (directed) { + for (int s = 0; s < n; s++) { + for (int i = 0; i < n; i++) dist[i] = -1; + int head = 0, tail = 0; dist[s] = 0; q[tail++] = s; + while (head < tail) { + int u = q[head++]; + for (int e = 0; e < a->outdeg[u]; e++) { + int w = a->out[u][e]; + if (dist[w] < 0) { dist[w] = dist[u] + 1; q[tail++] = w; } + } + } + for (int v = 0; v < n; v++) + if (v != s && dist[v] >= 0) { + Expr* ea[2] = { expr_copy((Expr*)verts->data.function.args[s]), + expr_copy((Expr*)verts->data.function.args[v]) }; + cedges[m++] = expr_new_function(expr_new_symbol(SYM_DirectedEdge), ea, 2); + } + } + } else { + /* Undirected: label components (BFS over out[] + in[]), then clique each. */ + int* comp = (n > 0) ? malloc((size_t)n * sizeof(int)) : NULL; + for (int i = 0; i < n; i++) comp[i] = -1; + int nc = 0; + for (int s = 0; s < n; s++) { + if (comp[s] >= 0) continue; + int head = 0, tail = 0; comp[s] = nc; q[tail++] = s; + while (head < tail) { + int u = q[head++]; + for (int pass = 0; pass < 2; pass++) { + int deg = pass ? a->indeg[u] : a->outdeg[u]; + int* nb = pass ? a->in[u] : a->out[u]; + for (int e = 0; e < deg; e++) + if (comp[nb[e]] < 0) { comp[nb[e]] = nc; q[tail++] = nb[e]; } + } + } + nc++; + } + for (int i = 0; i < n; i++) + for (int j = i + 1; j < n; j++) + if (comp[i] == comp[j]) { + Expr* ea[2] = { expr_copy((Expr*)verts->data.function.args[i]), + expr_copy((Expr*)verts->data.function.args[j]) }; + cedges[m++] = expr_new_function(expr_new_symbol(SYM_UndirectedEdge), ea, 2); + } + free(comp); + } + free(dist); free(q); + + Expr** vcopy = (n > 0) ? calloc((size_t)n, sizeof(Expr*)) : NULL; + for (int i = 0; i < n; i++) vcopy[i] = expr_copy((Expr*)verts->data.function.args[i]); + Expr* vlist = expr_new_function(expr_new_symbol(SYM_List), vcopy, (size_t)n); + Expr* elist = expr_new_function(expr_new_symbol(SYM_List), cedges, m); + free(vcopy); free(cedges); + graph_adj_free(a); + Expr* gargs[2] = { vlist, elist }; + return expr_new_function(expr_new_symbol(SYM_Graph), gargs, 2); +} diff --git a/src/graph/transitivereduction.c b/src/graph/transitivereduction.c new file mode 100644 index 00000000..f0da1784 --- /dev/null +++ b/src/graph/transitivereduction.c @@ -0,0 +1,81 @@ +/* transitivereduction.c - TransitiveReductionGraph[g]: the transitive reduction + * of a directed acyclic graph -- the graph on the same vertices with the fewest + * edges that preserves reachability. For a DAG this is unique: keep edge u->v + * iff there is no length->=2 path from u to v, i.e. no intermediate w with + * u ~> w ~> v. + * + * Reachability (paths of length >= 1) is found by a BFS from each vertex over + * the successor adjacency; self-reachability signals a cycle. Transitive + * reduction is only well defined (and preserving) on acyclic digraphs, so the + * call is left unevaluated when g has a directed cycle (an undirected edge is a + * 2-cycle, so any undirected edge also declines). O(V*(V+E) + E*V). + * + * Memory (SPEC section 4): returns a freshly-built Graph; frees res. NULL on a + * non-graph or a graph with a cycle. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +Expr* builtin_transitive_reduction_graph(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + + GraphAdj* a = graph_build_adj(g); + if (!a) return NULL; + int n = a->n; + + /* reach[u*n+v] = 1 iff v is reachable from u by a path of length >= 1. */ + char* reach = (n > 0) ? calloc((size_t)n * (size_t)n, 1) : NULL; + int* q = (n > 0) ? malloc((size_t)n * sizeof(int)) : NULL; + int cyclic = 0; + for (int s = 0; s < n; s++) { + char* row = reach + (size_t)s * n; + int head = 0, tail = 0; + for (int j = 0; j < a->outdeg[s]; j++) { + int w = a->out[s][j]; + if (!row[w]) { row[w] = 1; q[tail++] = w; } + } + while (head < tail) { + int v = q[head++]; + for (int j = 0; j < a->outdeg[v]; j++) { + int w = a->out[v][j]; + if (!row[w]) { row[w] = 1; q[tail++] = w; } + } + } + if (row[s]) { cyclic = 1; break; } /* s reaches itself -> cycle */ + } + free(q); + if (cyclic) { free(reach); graph_adj_free(a); return NULL; } + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + size_t m = edges->data.function.arg_count; + + Expr** vc = malloc((size_t)(n > 0 ? n : 1) * sizeof(Expr*)); + for (int i = 0; i < n; i++) vc[i] = expr_copy(verts->data.function.args[i]); + + Expr** ec = (m > 0) ? malloc(m * sizeof(Expr*)) : NULL; + size_t me = 0; + for (size_t k = 0; k < m; k++) { + const Expr* e = edges->data.function.args[k]; + int u = graph_vertex_index(verts, e->data.function.args[0]); + int v = graph_vertex_index(verts, e->data.function.args[1]); + int redundant = 0; + for (int w = 0; w < n && !redundant; w++) + if (w != u && w != v && reach[(size_t)u * n + w] && reach[(size_t)w * n + v]) + redundant = 1; + if (!redundant) ec[me++] = expr_copy((Expr*)e); + } + free(reach); + graph_adj_free(a); + + Expr* vlist = expr_new_function(expr_new_symbol(SYM_List), vc, (size_t)n); + Expr* elist = expr_new_function(expr_new_symbol(SYM_List), ec, me); + free(vc); free(ec); + Expr* gargs[2] = { vlist, elist }; + return expr_new_function(expr_new_symbol(SYM_Graph), gargs, 2); +} diff --git a/src/graph/treegraphq.c b/src/graph/treegraphq.c new file mode 100644 index 00000000..9b2d3af6 --- /dev/null +++ b/src/graph/treegraphq.c @@ -0,0 +1,60 @@ +/* treegraphq.c - TreeGraphQ[g]: True iff g is a tree -- a connected graph with + * no cycles. Equivalently: connected (on the underlying undirected graph) with + * exactly n-1 distinct edges, for n >= 1 vertices. + * + * A connected graph with n-1 edges is necessarily acyclic, so the two cheap + * checks -- underlying connectivity (one BFS) and a distinct-undirected-edge + * count of n-1 -- together characterise a tree without a separate cycle scan. + * Edge direction is ignored, and a->b together with b->a counts as one + * undirected edge (a boolean adjacency dedups them). O(V^2) via the adjacency + * matrix. A single vertex is a tree; the empty graph and any disconnected or + * edgeless multi-vertex graph is not. + * + * Memory (SPEC section 4): returns True/False; frees res. NULL on a non-graph. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +Expr* builtin_tree_graph_q(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + int n = (int)verts->data.function.arg_count; + size_t ne = edges->data.function.arg_count; + + if (n == 0) return expr_new_symbol(SYM_False); /* no vertices: not a tree */ + + char* adj = calloc((size_t)n * (size_t)n, 1); + long m = 0; /* distinct undirected edges */ + for (size_t k = 0; k < ne; k++) { + const Expr* e = edges->data.function.args[k]; + int ia = graph_vertex_index(verts, e->data.function.args[0]); + int ib = graph_vertex_index(verts, e->data.function.args[1]); + if (ia < 0 || ib < 0 || ia == ib) continue; + if (!adj[(size_t)ia * n + ib]) { + adj[(size_t)ia * n + ib] = adj[(size_t)ib * n + ia] = 1; + m++; + } + } + + /* BFS from vertex 0 over the undirected adjacency; count reached vertices. */ + char* seen = calloc((size_t)n, 1); + int* q = malloc((size_t)n * sizeof(int)); + int head = 0, tail = 0, reached = 0; + seen[0] = 1; q[tail++] = 0; + while (head < tail) { + int v = q[head++]; reached++; + for (int u = 0; u < n; u++) + if (adj[(size_t)v * n + u] && !seen[u]) { seen[u] = 1; q[tail++] = u; } + } + free(adj); free(seen); free(q); + + int is_tree = (reached == n) && (m == (long)n - 1); + return expr_new_symbol(is_tree ? SYM_True : SYM_False); +} diff --git a/src/graph/turangraph.c b/src/graph/turangraph.c new file mode 100644 index 00000000..66f0a181 --- /dev/null +++ b/src/graph/turangraph.c @@ -0,0 +1,48 @@ +/* turangraph.c - TuranGraph[n, r]: the Turan graph T(n, r), the complete + * r-partite graph on n vertices whose parts are as equal in size as possible. + * Two vertices are adjacent iff they lie in different parts, so it is the + * n-vertex graph with the most edges that contains no (r+1)-clique (Turan's + * theorem). + * + * Vertices are 1..n; vertex i (0-based) is placed in part i mod r, which yields + * balanced parts (sizes differ by at most 1). Undirected edges join every pair + * in different parts. O(n^2). T(n, 1) is edgeless, T(n, n) is K_n, T(4, 2) = C4, + * T(6, 3) is the octahedron. + * + * Memory (SPEC section 4): returns a freshly-built Graph; frees res. NULL unless + * n >= 0 and r >= 1 are integers. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +Expr* builtin_turan_graph(Expr* res) { + if (res->data.function.arg_count != 2) return NULL; + const Expr* ne = res->data.function.args[0]; + const Expr* re = res->data.function.args[1]; + if (ne->type != EXPR_INTEGER || re->type != EXPR_INTEGER) return NULL; + long n = (long)ne->data.integer, r = (long)re->data.integer; + if (n < 0 || r < 1) return NULL; + + Expr** vc = malloc((n > 0 ? (size_t)n : 1) * sizeof(Expr*)); + for (long i = 0; i < n; i++) vc[i] = expr_new_integer(i + 1); + + /* Upper bound on edges: n(n-1)/2. */ + size_t cap = (size_t)n * (n > 0 ? (size_t)(n - 1) : 0) / 2; + Expr** ec = (cap > 0) ? malloc(cap * sizeof(Expr*)) : NULL; + size_t me = 0; + for (long i = 0; i < n; i++) + for (long j = i + 1; j < n; j++) + if (i % r != j % r) { /* different parts */ + Expr* args[2] = { expr_new_integer(i + 1), expr_new_integer(j + 1) }; + ec[me++] = expr_new_function(expr_new_symbol(SYM_UndirectedEdge), args, 2); + } + + Expr* vlist = expr_new_function(expr_new_symbol(SYM_List), vc, (size_t)n); + Expr* elist = expr_new_function(expr_new_symbol(SYM_List), ec, me); + free(vc); free(ec); + Expr* gargs[2] = { vlist, elist }; + return expr_new_function(expr_new_symbol(SYM_Graph), gargs, 2); +} diff --git a/src/graph/vertexadd.c b/src/graph/vertexadd.c new file mode 100644 index 00000000..dfaee891 --- /dev/null +++ b/src/graph/vertexadd.c @@ -0,0 +1,52 @@ +/* vertexadd.c - VertexAdd[g, v] / VertexAdd[g, {v1, ...}]: g with the given + * vertex (or vertices) added as isolated vertices. The edge set is unchanged. + * + * A List second argument names several vertices to add (Wolfram's convention); + * any other expression is a single vertex. A vertex already present is not + * duplicated. O((V + #new) * #new). New vertices are appended after the existing + * ones. + * + * Memory (SPEC section 4): returns a freshly-built Graph; frees res. NULL on a + * non-graph first argument. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +Expr* builtin_vertex_add(Expr* res) { + if (res->data.function.arg_count != 2) return NULL; + const Expr* g = res->data.function.args[0]; + const Expr* spec = res->data.function.args[1]; + if (!graph_is_valid(g)) return NULL; + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + size_t n = verts->data.function.arg_count; + size_t m = edges->data.function.arg_count; + + /* New vertices: a List names several, anything else is a single vertex. */ + const Expr* const* adds; size_t na; const Expr* one[1]; + if (graph_is_list(spec)) { adds = (const Expr* const*)spec->data.function.args; na = spec->data.function.arg_count; } + else { one[0] = spec; adds = one; na = 1; } + + Expr** vc = malloc((n + na) * sizeof(Expr*)); + size_t nv = 0; + for (size_t i = 0; i < n; i++) vc[nv++] = expr_copy(verts->data.function.args[i]); + for (size_t i = 0; i < na; i++) { + const Expr* v = adds[i]; + int seen = 0; + for (size_t j = 0; j < nv; j++) if (expr_eq(vc[j], v)) { seen = 1; break; } + if (!seen) vc[nv++] = expr_copy((Expr*)v); + } + + Expr** ec = (m > 0) ? malloc(m * sizeof(Expr*)) : NULL; + for (size_t i = 0; i < m; i++) ec[i] = expr_copy(edges->data.function.args[i]); + + Expr* vlist = expr_new_function(expr_new_symbol(SYM_List), vc, nv); + Expr* elist = expr_new_function(expr_new_symbol(SYM_List), ec, m); + free(vc); free(ec); + Expr* gargs[2] = { vlist, elist }; + return expr_new_function(expr_new_symbol(SYM_Graph), gargs, 2); +} diff --git a/src/graph/vertexcomponents.c b/src/graph/vertexcomponents.c new file mode 100644 index 00000000..e85f5389 --- /dev/null +++ b/src/graph/vertexcomponents.c @@ -0,0 +1,62 @@ +/* vertexcomponents.c - VertexOutComponent[g, v] and VertexInComponent[g, v]. + * + * VertexOutComponent[g, v] the vertices reachable FROM v following edges + * forward (over out-edges), including v itself. + * VertexInComponent[g, v] the vertices from which v is reachable (over + * in-edges), including v itself. + * + * Each is a single BFS from v over the appropriate direction of the adjacency, + * O(V+E); results are vertices in the graph's canonical order. For an undirected + * graph out- and in-neighbourhoods coincide, so both give v's connected + * component. A vertex not in g leaves the call unevaluated. + * + * Memory (SPEC section 4): returns a fresh List; frees res. NULL on a non-graph + * or an unknown vertex. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +/* Shared: BFS from vertex `res`'s second argument over out[] (use_in=0) or + * in[] (use_in=1); returns the reachable vertices as a List, or NULL. */ +static Expr* component(Expr* res, int use_in) { + if (res->data.function.arg_count != 2) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + const Expr* verts = g->data.function.args[0]; + int src = graph_vertex_index(verts, res->data.function.args[1]); + if (src < 0) return NULL; + + GraphAdj* a = graph_build_adj(g); + if (!a) return NULL; + int n = a->n; + int** nbr = use_in ? a->in : a->out; + int* deg = use_in ? a->indeg : a->outdeg; + + char* seen = calloc((size_t)n, 1); + int* q = malloc((size_t)n * sizeof(int)); + int head = 0, tail = 0; + seen[src] = 1; q[tail++] = src; + while (head < tail) { + int v = q[head++]; + for (int j = 0; j < deg[v]; j++) { + int u = nbr[v][j]; + if (!seen[u]) { seen[u] = 1; q[tail++] = u; } + } + } + free(q); + graph_adj_free(a); + + Expr** items = malloc((size_t)n * sizeof(Expr*)); + size_t cnt = 0; + for (int i = 0; i < n; i++) if (seen[i]) items[cnt++] = expr_copy(verts->data.function.args[i]); + free(seen); + Expr* out = expr_new_function(expr_new_symbol(SYM_List), items, cnt); + free(items); + return out; +} + +Expr* builtin_vertex_out_component(Expr* res) { return component(res, 0); } +Expr* builtin_vertex_in_component(Expr* res) { return component(res, 1); } diff --git a/src/graph/vertexcontract.c b/src/graph/vertexcontract.c new file mode 100644 index 00000000..dc8cdd66 --- /dev/null +++ b/src/graph/vertexcontract.c @@ -0,0 +1,92 @@ +/* vertexcontract.c - VertexContract[g, {v1, v2, ...}]: merge the listed vertices + * into a single vertex (the first of the list), redirecting every incident edge + * to the representative, deleting the resulting self-loops, and collapsing + * parallel edges. + * + * The representative keeps its position; the other listed vertices are removed. + * Each edge endpoint that names a merged vertex is rewritten to the + * representative; an edge whose endpoints then coincide is a self-loop and is + * dropped; remaining edges are deduplicated with edge-kind-aware, + * symmetric-for-undirected equality. O((V+E)*k + E^2) at small-graph scale. + * Contracting the two endpoints of an edge realises edge contraction (e.g. a + * triangle becomes a single edge). + * + * Memory (SPEC section 4): returns a freshly-built Graph; frees res. NULL unless + * arg1 is a valid graph and arg2 a List of its vertices. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +static int same_edge(const Expr* a, const Expr* b) { + const char* ka = graph_edge_kind(a); + const char* kb = graph_edge_kind(b); + if (!ka || ka != kb) return 0; + const Expr* a0 = a->data.function.args[0]; + const Expr* a1 = a->data.function.args[1]; + const Expr* b0 = b->data.function.args[0]; + const Expr* b1 = b->data.function.args[1]; + if (expr_eq(a0, b0) && expr_eq(a1, b1)) return 1; + if (ka == SYM_UndirectedEdge && expr_eq(a0, b1) && expr_eq(a1, b0)) return 1; + return 0; +} + +/* Is v one of the merged vertices (list elements)? */ +static int in_set(const Expr* set, const Expr* v) { + for (size_t i = 0; i < set->data.function.arg_count; i++) + if (expr_eq(set->data.function.args[i], v)) return 1; + return 0; +} + +Expr* builtin_vertex_contract(Expr* res) { + if (res->data.function.arg_count != 2) return NULL; + const Expr* g = res->data.function.args[0]; + const Expr* set = res->data.function.args[1]; + if (!graph_is_valid(g) || !graph_is_list(set) || set->data.function.arg_count == 0) + return NULL; + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + size_t n = verts->data.function.arg_count; + size_t m = edges->data.function.arg_count; + const Expr* rep = set->data.function.args[0]; + + /* Every listed vertex must be a vertex of g. */ + for (size_t i = 0; i < set->data.function.arg_count; i++) + if (graph_vertex_index(verts, set->data.function.args[i]) < 0) return NULL; + + /* New vertices: keep rep and every non-merged vertex (order preserved). */ + Expr** vc = malloc((n > 0 ? n : 1) * sizeof(Expr*)); + size_t nv = 0; + for (size_t i = 0; i < n; i++) { + const Expr* v = verts->data.function.args[i]; + if (in_set(set, v) && !expr_eq(v, rep)) continue; /* absorbed */ + vc[nv++] = expr_copy((Expr*)v); + } + + /* Remap edges, drop self-loops, dedup. */ + Expr** ec = malloc((m > 0 ? m : 1) * sizeof(Expr*)); + size_t me = 0; + for (size_t i = 0; i < m; i++) { + const Expr* e = edges->data.function.args[i]; + const char* kind = graph_edge_kind(e); + const Expr* a = e->data.function.args[0]; + const Expr* b = e->data.function.args[1]; + const Expr* na = in_set(set, a) ? rep : a; + const Expr* nb = in_set(set, b) ? rep : b; + if (expr_eq(na, nb)) continue; /* self-loop */ + Expr* ne = expr_new_function(expr_new_symbol(kind), + (Expr*[]){ expr_copy((Expr*)na), expr_copy((Expr*)nb) }, 2); + int dup = 0; + for (size_t j = 0; j < me; j++) if (same_edge(ec[j], ne)) { dup = 1; break; } + if (dup) expr_free(ne); else ec[me++] = ne; + } + + Expr* vlist = expr_new_function(expr_new_symbol(SYM_List), vc, nv); + Expr* elist = expr_new_function(expr_new_symbol(SYM_List), ec, me); + free(vc); free(ec); + Expr* gargs[2] = { vlist, elist }; + return expr_new_function(expr_new_symbol(SYM_Graph), gargs, 2); +} diff --git a/src/graph/vertexcoreness.c b/src/graph/vertexcoreness.c new file mode 100644 index 00000000..9f8e4a2c --- /dev/null +++ b/src/graph/vertexcoreness.c @@ -0,0 +1,65 @@ +/* vertexcoreness.c - VertexCoreness[g]: for each vertex, its coreness (core + * number) -- the largest k such that the vertex still belongs to the k-core (the + * maximal subgraph of minimum degree k). + * + * Batagelj-Zaversnik peeling: repeatedly remove a vertex of minimum current + * degree; the running maximum of those removal-degrees is the core level, and + * each removed vertex is assigned the current level. Edge direction is ignored + * (coreness is defined on the underlying undirected graph). Results are exact + * integers in vertex order. O(V^2) with a linear min-degree scan (small-graph + * scale). Complements KCoreComponents (a vertex has coreness >= k iff it appears + * in KCoreComponents[g, k]). + * + * Memory (SPEC section 4): returns a fresh List; frees res. NULL on a non-graph. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +Expr* builtin_vertex_coreness(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + int n = (int)verts->data.function.arg_count; + size_t ne = edges->data.function.arg_count; + + char* adj = (n > 0) ? calloc((size_t)n * (size_t)n, 1) : NULL; + int* deg = (n > 0) ? calloc((size_t)n, sizeof(int)) : NULL; + for (size_t e = 0; e < ne; e++) { + const Expr* ed = edges->data.function.args[e]; + int ia = graph_vertex_index(verts, ed->data.function.args[0]); + int ib = graph_vertex_index(verts, ed->data.function.args[1]); + if (ia < 0 || ib < 0 || ia == ib) continue; + if (!adj[(size_t)ia * n + ib]) { + adj[(size_t)ia * n + ib] = adj[(size_t)ib * n + ia] = 1; + deg[ia]++; deg[ib]++; + } + } + + int* core = (n > 0) ? calloc((size_t)n, sizeof(int)) : NULL; + char* removed = (n > 0) ? calloc((size_t)n, 1) : NULL; + int level = 0; + for (int step = 0; step < n; step++) { + int v = -1, best = 0; + for (int i = 0; i < n; i++) + if (!removed[i] && (v < 0 || deg[i] < best)) { v = i; best = deg[i]; } + if (best > level) level = best; /* running max = core level */ + core[v] = level; + removed[v] = 1; + for (int u = 0; u < n; u++) + if (!removed[u] && adj[(size_t)v * n + u]) deg[u]--; + } + free(adj); free(deg); free(removed); + + Expr** items = (n > 0) ? calloc((size_t)n, sizeof(Expr*)) : NULL; + for (int i = 0; i < n; i++) items[i] = expr_new_integer(core[i]); + free(core); + Expr* out = expr_new_function(expr_new_symbol(SYM_List), items, (size_t)n); + free(items); + return out; +} diff --git a/src/graph/vertexdelete.c b/src/graph/vertexdelete.c new file mode 100644 index 00000000..4ab376d5 --- /dev/null +++ b/src/graph/vertexdelete.c @@ -0,0 +1,67 @@ +/* vertexdelete.c - VertexDelete[g, v] / VertexDelete[g, {v1, ...}]: g with the + * given vertex (or vertices) removed, along with every edge incident to a removed + * vertex. + * + * A List second argument names several vertices to delete (matching Wolfram's + * convention); any other expression is a single vertex. Surviving vertices keep + * their order and edge kinds are preserved. O(V + E) with linear membership + * tests. VertexDelete[K4, 1] is K3; VertexDelete[PathGraph[3], 2] leaves two + * isolated vertices. + * + * Memory (SPEC section 4): returns a freshly-built Graph; frees res. NULL on a + * non-graph first argument. + */ + +#include "graph.h" +#include "expr.h" +#include "sym_names.h" +#include + +/* Is vertex-expression v named by the delete spec? */ +static int in_delete_spec(const Expr* spec, const Expr* v) { + if (graph_is_list(spec)) { + for (size_t i = 0; i < spec->data.function.arg_count; i++) + if (expr_eq(spec->data.function.args[i], v)) return 1; + return 0; + } + return expr_eq(spec, v); +} + +Expr* builtin_vertex_delete(Expr* res) { + if (res->data.function.arg_count != 2) return NULL; + const Expr* g = res->data.function.args[0]; + const Expr* spec = res->data.function.args[1]; + if (!graph_is_valid(g)) return NULL; + + const Expr* verts = g->data.function.args[0]; + const Expr* edges = g->data.function.args[1]; + int n = (int)verts->data.function.arg_count; + size_t m = edges->data.function.arg_count; + + char* removed = (n > 0) ? calloc((size_t)n, 1) : NULL; + Expr** vc = malloc((size_t)(n > 0 ? n : 1) * sizeof(Expr*)); + size_t nv = 0; + for (int i = 0; i < n; i++) { + const Expr* v = verts->data.function.args[i]; + if (in_delete_spec(spec, v)) { removed[i] = 1; continue; } + vc[nv++] = expr_copy((Expr*)v); + } + + Expr** ec = (m > 0) ? malloc(m * sizeof(Expr*)) : NULL; + size_t me = 0; + for (size_t k = 0; k < m; k++) { + const Expr* e = edges->data.function.args[k]; + int a = graph_vertex_index(verts, e->data.function.args[0]); + int b = graph_vertex_index(verts, e->data.function.args[1]); + if (a >= 0 && removed[a]) continue; + if (b >= 0 && removed[b]) continue; + ec[me++] = expr_copy((Expr*)e); + } + free(removed); + + Expr* vlist = expr_new_function(expr_new_symbol(SYM_List), vc, nv); + Expr* elist = expr_new_function(expr_new_symbol(SYM_List), ec, me); + free(vc); free(ec); + Expr* gargs[2] = { vlist, elist }; + return expr_new_function(expr_new_symbol(SYM_Graph), gargs, 2); +} diff --git a/src/graph/vertexlist.c b/src/graph/vertexlist.c new file mode 100644 index 00000000..d6d5cec0 --- /dev/null +++ b/src/graph/vertexlist.c @@ -0,0 +1,13 @@ +/* vertexlist.c - VertexList[g]: the graph's vertices, in canonical order. + * Thin reader over the canonical form; NULL (unevaluated) if g is not a graph. + * Memory (SPEC section 4): returns a fresh copy; the evaluator frees res. */ + +#include "graph.h" +#include "expr.h" + +Expr* builtin_vertex_list(Expr* res) { + if (res->data.function.arg_count != 1) return NULL; + const Expr* g = res->data.function.args[0]; + if (!graph_is_valid(g)) return NULL; + return expr_copy(g->data.function.args[0]); +} diff --git a/src/graphics/graphics_json.c b/src/graphics/graphics_json.c index 8c81e9a9..3b15064a 100644 --- a/src/graphics/graphics_json.c +++ b/src/graphics/graphics_json.c @@ -21,6 +21,13 @@ * $PlotResample[...] * ] * + * GraphPlot[] instead emits node-link diagrams built from Line (edges), + * Disk (vertices), and Text (labels). When Disk or Text primitives are + * present the converter switches to "diagram mode": vertices become marker + * points, labels become annotations, and the layout uses an equal-aspect + * (scaleanchor) square with hidden axes -- so a graph draws as a graph, not + * as an XY function plot. + * * Output JSON (Plotly format): * * { @@ -29,13 +36,7 @@ * "line":{"color":"rgba(51,102,204,1.0)","width":1.5}}, * ... * ], - * "layout": { - * "xaxis":{"showgrid":true,"zeroline":true}, - * "yaxis":{"showgrid":true,"zeroline":true}, - * "margin":{"l":50,"r":20,"t":20,"b":50}, - * "height":350, - * "plot_bgcolor":"#fff","paper_bgcolor":"#fff" - * } + * "layout": { ... } * } */ @@ -83,9 +84,12 @@ static int buf_cat(Buf* b, const char* s) { return 1; } -/* Append a double value — use %g to keep it compact. */ +/* Append a double value — use %g to keep it compact. Snap sub-noise + * magnitudes to 0 so floating-point dust (e.g. sin(pi) ~ 1e-16) does not + * blow up an auto-ranged axis. */ static int buf_catd(Buf* b, double v) { char tmp[64]; + if (fabs(v) < 1e-12) v = 0.0; snprintf(tmp, sizeof(tmp), "%.7g", v); return buf_cat(b, tmp); } @@ -111,6 +115,38 @@ static int expr_to_double(const Expr* e, double* out) { return 0; } +/* Read a coordinate pair List[x, y] into x/y. */ +static int get_xy(const Expr* pair, double* x, double* y) { + if (!head_is(pair, SYM_List) || pair->data.function.arg_count < 2) return 0; + return expr_to_double(pair->data.function.args[0], x) + && expr_to_double(pair->data.function.args[1], y); +} + +/* Append a JSON string literal (with surrounding quotes) for a text label, + * escaping the characters JSON requires. */ +static void append_label(Buf* b, const Expr* e) { + char tmp[64]; + const char* s = NULL; + if (e && e->type == EXPR_STRING) s = e->data.string; + else if (e && e->type == EXPR_SYMBOL) s = e->data.symbol; + else if (e && e->type == EXPR_INTEGER) { + snprintf(tmp, sizeof(tmp), "%lld", (long long)e->data.integer); s = tmp; + } else { + double d; + if (e && expr_to_double(e, &d)) { snprintf(tmp, sizeof(tmp), "%.7g", d); s = tmp; } + else s = "?"; + } + buf_cat(b, "\""); + for (; *s; s++) { + unsigned char c = (unsigned char)*s; + if (c == '"' || c == '\\') { char e2[3] = {'\\', (char)c, 0}; buf_cat(b, e2); } + else if (c == '\n') buf_cat(b, "\\n"); + else if (c < 0x20) { char u[8]; snprintf(u, sizeof(u), "\\u%04x", c); buf_cat(b, u); } + else { char one[2] = {(char)c, 0}; buf_cat(b, one); } + } + buf_cat(b, "\""); +} + /* ----------------------------------------------------------------------- * Coordinate-list serialiser * Appends "[x1,x2,...]" or "[y1,y2,...]" to buf from @@ -159,8 +195,22 @@ char* graphics_to_plotly_json(const Expr* g) { const Expr* prim_list = g->data.function.args[0]; if (!head_is(prim_list, SYM_List)) return NULL; - Buf data_buf; + size_t n = prim_list->data.function.arg_count; + + /* Diagram mode: a node-link picture (GraphPlot) rather than a function + * plot. Signalled by the presence of Disk (vertices) or Text (labels), + * which Plot[] never emits. */ + int diagram_mode = 0; + for (size_t i = 0; i < n; i++) { + const Expr* p = prim_list->data.function.args[i]; + if (head_is(p, SYM_Disk) || head_is(p, SYM_Text)) { diagram_mode = 1; break; } + } + + Buf data_buf, anno_buf; if (!buf_init(&data_buf, 65536)) return NULL; + if (!buf_init(&anno_buf, 4096)) { buf_free(&data_buf); return NULL; } + buf_cat(&anno_buf, "["); + int first_anno = 1; /* Track current drawing state. */ double cur_r = 0.2, cur_g = 0.4, cur_b = 0.8; /* Mathematica default blue */ @@ -169,7 +219,6 @@ char* graphics_to_plotly_json(const Expr* g) { buf_cat(&data_buf, "["); - size_t n = prim_list->data.function.arg_count; for (size_t i = 0; i < n; i++) { const Expr* p = prim_list->data.function.args[i]; if (!p) continue; @@ -211,7 +260,9 @@ char* graphics_to_plotly_json(const Expr* g) { append_coord_array(&data_buf, pts, 1); buf_cat(&data_buf, ",\"line\":{\"color\":\""); buf_cat(&data_buf, color_str); - buf_cat(&data_buf, "\",\"width\":1.5},\"showlegend\":false}"); + /* Diagram edges a touch heavier; plot curves stay thin. */ + buf_cat(&data_buf, diagram_mode ? "\",\"width\":2},\"hoverinfo\":\"skip\",\"showlegend\":false}" + : "\",\"width\":1.5},\"showlegend\":false}"); continue; } @@ -238,35 +289,222 @@ char* graphics_to_plotly_json(const Expr* g) { continue; } + /* ----- Disk[{x,y}, r] — a vertex marker ---------------------- */ + if (head_is(p, SYM_Disk) && p->data.function.arg_count >= 1) { + double x, y; + if (!get_xy(p->data.function.args[0], &x, &y)) continue; + + /* Marker px size scales with the disk radius so VertexSize takes + * effect; the default radius (0.08) maps to the historical 22px. */ + double radius = 0.08, msize = 22.0; + if (p->data.function.arg_count >= 2 + && expr_to_double(p->data.function.args[1], &radius)) { + msize = radius * 275.0; + if (msize < 6.0) msize = 6.0; + if (msize > 90.0) msize = 90.0; + } + + char color_str[64]; + rgba_str(color_str, sizeof(color_str), cur_r, cur_g, cur_b, 1.0); + + if (!first_trace) buf_cat(&data_buf, ","); + first_trace = 0; + + buf_cat(&data_buf, "{\"type\":\"scatter\",\"mode\":\"markers\",\"x\":["); + buf_catd(&data_buf, x); + buf_cat(&data_buf, "],\"y\":["); + buf_catd(&data_buf, y); + buf_cat(&data_buf, "],\"marker\":{\"size\":"); + buf_catd(&data_buf, msize); + buf_cat(&data_buf, ",\"color\":\""); + buf_cat(&data_buf, color_str); + buf_cat(&data_buf, "\"},\"hoverinfo\":\"skip\",\"showlegend\":false}"); + continue; + } + + /* ----- Point[List[pts...]] — marker points ------------------- */ + if (head_is(p, SYM_Point) && p->data.function.arg_count >= 1) { + const Expr* pts = p->data.function.args[0]; + if (!head_is(pts, SYM_List)) continue; + + char color_str[64]; + rgba_str(color_str, sizeof(color_str), cur_r, cur_g, cur_b, 1.0); + + if (!first_trace) buf_cat(&data_buf, ","); + first_trace = 0; + + buf_cat(&data_buf, "{\"type\":\"scatter\",\"mode\":\"markers\",\"x\":"); + append_coord_array(&data_buf, pts, 0); + buf_cat(&data_buf, ",\"y\":"); + append_coord_array(&data_buf, pts, 1); + buf_cat(&data_buf, ",\"marker\":{\"size\":8,\"color\":\""); + buf_cat(&data_buf, color_str); + buf_cat(&data_buf, "\"},\"showlegend\":false}"); + continue; + } + + /* ----- Text[label, {x,y}] — an annotation -------------------- */ + if (head_is(p, SYM_Text) && p->data.function.arg_count >= 2) { + double x, y; + if (!get_xy(p->data.function.args[1], &x, &y)) continue; + + if (!first_anno) buf_cat(&anno_buf, ","); + first_anno = 0; + + buf_cat(&anno_buf, "{\"x\":"); + buf_catd(&anno_buf, x); + buf_cat(&anno_buf, ",\"y\":"); + buf_catd(&anno_buf, y); + buf_cat(&anno_buf, ",\"text\":"); + append_label(&anno_buf, p->data.function.args[0]); + /* White label centred on the vertex marker. */ + buf_cat(&anno_buf, ",\"showarrow\":false,\"font\":{\"color\":\"#fff\",\"size\":12}}"); + continue; + } + /* All other primitives (Rule, $PlotResample, etc.) — skip. */ } buf_cat(&data_buf, "]"); + buf_cat(&anno_buf, "]"); /* Build the layout object. */ - char layout[512]; - snprintf(layout, sizeof(layout), - "{\"xaxis\":{\"showgrid\":true,\"zeroline\":true," - "\"zerolinecolor\":\"#aaa\",\"zerolinewidth\":1}," - "\"yaxis\":{\"showgrid\":true,\"zeroline\":true," - "\"zerolinecolor\":\"#aaa\",\"zerolinewidth\":1}," - "\"margin\":{\"l\":50,\"r\":20,\"t\":20,\"b\":50}," - "\"height\":350," - "\"plot_bgcolor\":\"#fff\"," - "\"paper_bgcolor\":\"#fff\"}"); + Buf layout; + if (!buf_init(&layout, 1024)) { buf_free(&data_buf); buf_free(&anno_buf); return NULL; } + if (diagram_mode) { + /* Square, axis-free canvas with equal x/y scaling so a circular + * vertex layout stays circular. */ + buf_cat(&layout, + "{\"xaxis\":{\"visible\":false,\"showgrid\":false,\"zeroline\":false}," + "\"yaxis\":{\"visible\":false,\"showgrid\":false,\"zeroline\":false," + "\"scaleanchor\":\"x\",\"scaleratio\":1}," + "\"margin\":{\"l\":20,\"r\":20,\"t\":20,\"b\":20}," + "\"height\":400,\"showlegend\":false," + "\"plot_bgcolor\":\"#fff\",\"paper_bgcolor\":\"#fff\","); + buf_cat(&layout, "\"annotations\":"); + buf_cat(&layout, anno_buf.buf); + buf_cat(&layout, "}"); + } else { + buf_cat(&layout, + "{\"xaxis\":{\"showgrid\":true,\"zeroline\":true," + "\"zerolinecolor\":\"#aaa\",\"zerolinewidth\":1}," + "\"yaxis\":{\"showgrid\":true,\"zeroline\":true," + "\"zerolinecolor\":\"#aaa\",\"zerolinewidth\":1}," + "\"margin\":{\"l\":50,\"r\":20,\"t\":20,\"b\":50}," + "\"height\":350," + "\"plot_bgcolor\":\"#fff\",\"paper_bgcolor\":\"#fff\"}"); + } /* Assemble the final JSON object. */ Buf out; - if (!buf_init(&out, data_buf.len + 1024)) { - buf_free(&data_buf); + if (!buf_init(&out, data_buf.len + layout.len + 64)) { + buf_free(&data_buf); buf_free(&anno_buf); buf_free(&layout); return NULL; } buf_cat(&out, "{\"data\":"); buf_cat(&out, data_buf.buf); buf_cat(&out, ",\"layout\":"); - buf_cat(&out, layout); + buf_cat(&out, layout.buf); buf_cat(&out, "}"); + buf_free(&data_buf); + buf_free(&anno_buf); + buf_free(&layout); + return out.buf; /* caller frees */ +} + +/* ----------------------------------------------------------------------- + * Graphics3D → Plotly scatter3d (Graph3D node-link diagrams) + * --------------------------------------------------------------------- */ + +static int get_xyz(const Expr* t, double* x, double* y, double* z) { + if (!head_is(t, SYM_List) || t->data.function.arg_count < 3) return 0; + return expr_to_double(t->data.function.args[0], x) + && expr_to_double(t->data.function.args[1], y) + && expr_to_double(t->data.function.args[2], z); +} + +char* graphics3d_to_plotly_json(const Expr* g) { + if (!g || !head_is(g, SYM_Graphics3D) || g->data.function.arg_count < 1) + return NULL; + const Expr* prim_list = g->data.function.args[0]; + if (!head_is(prim_list, SYM_List)) return NULL; + size_t n = prim_list->data.function.arg_count; + + Buf data_buf; + if (!buf_init(&data_buf, 65536)) return NULL; + buf_cat(&data_buf, "["); + + double cr = 0.24, cg = 0.52, cb = 0.90; /* current colour */ + int first_trace = 1; + + for (size_t i = 0; i < n; i++) { + const Expr* p = prim_list->data.function.args[i]; + if (!p) continue; + + if (head_is(p, SYM_RGBColor) && p->data.function.arg_count >= 3) { + double r, gg, b; + if (expr_to_double(p->data.function.args[0], &r) + && expr_to_double(p->data.function.args[1], &gg) + && expr_to_double(p->data.function.args[2], &b)) { + cr = r; cg = gg; cb = b; + } + continue; + } + + /* Line[List[{x,y,z},{x,y,z}]] — a 3D edge segment. */ + if (head_is(p, SYM_Line) && p->data.function.arg_count >= 1) { + const Expr* pts = p->data.function.args[0]; + if (!head_is(pts, SYM_List) || pts->data.function.arg_count < 2) continue; + double x1,y1,z1,x2,y2,z2; + if (!get_xyz(pts->data.function.args[0], &x1,&y1,&z1)) continue; + if (!get_xyz(pts->data.function.args[1], &x2,&y2,&z2)) continue; + char color[64]; rgba_str(color, sizeof(color), cr, cg, cb, 1.0); + if (!first_trace) buf_cat(&data_buf, ","); first_trace = 0; + buf_cat(&data_buf, "{\"type\":\"scatter3d\",\"mode\":\"lines\",\"x\":["); + buf_catd(&data_buf, x1); buf_cat(&data_buf, ","); buf_catd(&data_buf, x2); + buf_cat(&data_buf, "],\"y\":["); + buf_catd(&data_buf, y1); buf_cat(&data_buf, ","); buf_catd(&data_buf, y2); + buf_cat(&data_buf, "],\"z\":["); + buf_catd(&data_buf, z1); buf_cat(&data_buf, ","); buf_catd(&data_buf, z2); + buf_cat(&data_buf, "],\"line\":{\"color\":\""); buf_cat(&data_buf, color); + buf_cat(&data_buf, "\",\"width\":3},\"hoverinfo\":\"skip\",\"showlegend\":false}"); + continue; + } + + /* Point[List[{x,y,z}, ...]] — 3D vertex markers. */ + if (head_is(p, SYM_Point) && p->data.function.arg_count >= 1) { + const Expr* pts = p->data.function.args[0]; + if (!head_is(pts, SYM_List)) continue; + size_t m = pts->data.function.arg_count; + char color[64]; rgba_str(color, sizeof(color), cr, cg, cb, 1.0); + if (!first_trace) buf_cat(&data_buf, ","); first_trace = 0; + buf_cat(&data_buf, "{\"type\":\"scatter3d\",\"mode\":\"markers\",\"x\":["); + for (size_t j = 0; j < m; j++) { double x,y,z; if (!get_xyz(pts->data.function.args[j],&x,&y,&z)) continue; if (j) buf_cat(&data_buf, ","); buf_catd(&data_buf, x); } + buf_cat(&data_buf, "],\"y\":["); + for (size_t j = 0; j < m; j++) { double x,y,z; if (!get_xyz(pts->data.function.args[j],&x,&y,&z)) continue; if (j) buf_cat(&data_buf, ","); buf_catd(&data_buf, y); } + buf_cat(&data_buf, "],\"z\":["); + for (size_t j = 0; j < m; j++) { double x,y,z; if (!get_xyz(pts->data.function.args[j],&x,&y,&z)) continue; if (j) buf_cat(&data_buf, ","); buf_catd(&data_buf, z); } + buf_cat(&data_buf, "],\"marker\":{\"size\":5,\"color\":\""); buf_cat(&data_buf, color); + buf_cat(&data_buf, "\"},\"hoverinfo\":\"skip\",\"showlegend\":false}"); + continue; + } + } + buf_cat(&data_buf, "]"); + + Buf out; + if (!buf_init(&out, data_buf.len + 512)) { buf_free(&data_buf); return NULL; } + buf_cat(&out, "{\"data\":"); + buf_cat(&out, data_buf.buf); + buf_cat(&out, + ",\"layout\":{\"scene\":{" + "\"xaxis\":{\"visible\":false}," + "\"yaxis\":{\"visible\":false}," + "\"zaxis\":{\"visible\":false}," + "\"aspectmode\":\"data\"}," + "\"margin\":{\"l\":0,\"r\":0,\"t\":0,\"b\":0}," + "\"showlegend\":false," + "\"paper_bgcolor\":\"#fff\"}}"); buf_free(&data_buf); return out.buf; /* caller frees */ } diff --git a/src/graphics/graphics_json.h b/src/graphics/graphics_json.h index be268f5c..ccd6c7f5 100644 --- a/src/graphics/graphics_json.h +++ b/src/graphics/graphics_json.h @@ -20,4 +20,16 @@ */ char* graphics_to_plotly_json(const Expr* g); +/* + * graphics3d_to_plotly_json(g) + * + * Convert a Graphics3D[...] Expr (as produced by Graph3D) to a Plotly + * scatter3d scene JSON string. Handled primitives: + * Line[List[List[x,y,z], List[x,y,z]]] — a 3D line segment (edge) + * Point[List[List[x,y,z], ...]] — 3D markers (vertices) + * RGBColor[r, g, b] — current colour + * Returns a heap-allocated string (caller frees) or NULL. + */ +char* graphics3d_to_plotly_json(const Expr* g); + #endif /* MATHILDA_GRAPHICS_JSON_H */ diff --git a/src/parse.c b/src/parse.c index cef393a3..abdd8140 100644 --- a/src/parse.c +++ b/src/parse.c @@ -706,6 +706,7 @@ typedef enum { OP_APPLY1, OP_RULE, OP_RULEDELAYED, + OP_TWOWAYRULE, OP_CONDITION, OP_ALTERNATIVES, OP_MAP, @@ -765,6 +766,11 @@ static OperatorDef get_operator(const char* pos) { def.type = OP_REPLACEREPEATED; def.prec = 110; def.right_assoc = 0; def.head_name = "ReplaceRepeated"; def.len = 3; } else if (strncmp(pos, "//@", 3) == 0) { def.type = OP_MAPALL; def.prec = 620; def.right_assoc = 1; def.head_name = "MapAll"; def.len = 3; + } else if (strncmp(pos, "<->", 3) == 0) { + /* TwoWayRule (u <-> v). Same precedence/associativity as Rule; the + * graph subsystem normalizes it to UndirectedEdge on construction. + * Checked before "<>", "<=", "<" so the 3-char form wins. */ + def.type = OP_TWOWAYRULE; def.prec = 120; def.right_assoc = 1; def.head_name = "TwoWayRule"; def.len = 3; } else if (strncmp(pos, "//", 2) == 0) { def.type = OP_POSTFIX; def.prec = 70; def.head_name = "Postfix"; def.len = 2; } else if (strncmp(pos, "/.", 2) == 0 && !isdigit(pos[2])) { diff --git a/src/print.c b/src/print.c index 80f4958d..270e8bab 100644 --- a/src/print.c +++ b/src/print.c @@ -12,6 +12,7 @@ #include "arithmetic.h" #include "context.h" #include "sym_names.h" +#include "graph.h" /* graph_is_list, for the Graph[...] summary form */ #include #include #include @@ -50,6 +51,7 @@ static int get_expr_prec(Expr* e) { if (head == SYM_Set || head == SYM_SetDelayed) return 40; if (head == SYM_MessageName) return 780; if (head == SYM_Rule || head == SYM_RuleDelayed) return 120; + if (head == SYM_DirectedEdge || head == SYM_UndirectedEdge) return 120; if (head == SYM_Condition) return 130; if (head == SYM_Alternatives) return 160; if (head == SYM_Repeated || head == SYM_RepeatedNull) return 170; @@ -188,6 +190,26 @@ static void print_standard(Expr* e, int parent_prec) { else if (head == SYM_Graphics && e->data.function.arg_count >= 1 && g_inputform_depth == 0) { printf("-Graphics-"); } + else if ((head == SYM_DirectedEdge || head == SYM_UndirectedEdge) + && e->data.function.arg_count == 2) { + /* u -> v (directed) / u <-> v (undirected). Infix in both Standard + * and Input forms so the literal round-trips through the parser. */ + print_standard(e->data.function.args[0], my_prec); + printf("%s", head == SYM_DirectedEdge ? " -> " : " <-> "); + print_standard(e->data.function.args[1], my_prec); + } + else if (head == SYM_Graph && e->data.function.arg_count == 2 + && g_inputform_depth == 0 + && graph_is_list(e->data.function.args[0]) + && graph_is_list(e->data.function.args[1])) { + /* Terse summary in standard output; InputForm/FullForm fall through + * to the literal Graph[{...}, {...}] constructor (round-trippable). */ + unsigned long nv = (unsigned long)e->data.function.args[0]->data.function.arg_count; + unsigned long ne = (unsigned long)e->data.function.args[1]->data.function.arg_count; + printf("Graph[<%lu %s, %lu %s>]", + nv, nv == 1 ? "vertex" : "vertices", + ne, ne == 1 ? "edge" : "edges"); + } else if (head == SYM_Rational && e->data.function.arg_count == 2) { print_standard(e->data.function.args[0], 470); printf("/"); diff --git a/src/repl.c b/src/repl.c index 7d6e153d..d705de55 100644 --- a/src/repl.c +++ b/src/repl.c @@ -8,6 +8,7 @@ #include "sym_names.h" #include "show.h" #include "graphics_json.h" +#include "graph.h" #include "print_latex.h" #include #include @@ -144,6 +145,16 @@ void process_input(const char* input, int line_number) { && to_print->data.function.arg_count >= 1) { graphics_show(to_print); } + /* A bare Graph result auto-displays as a diagram, the same way Graphics + * does: render it to a temporary Graphics tree and hand that to the + * renderer (which we own and free here). */ + else if (to_print && to_print->type == EXPR_FUNCTION + && to_print->data.function.head->type == EXPR_SYMBOL + && to_print->data.function.head->data.symbol == SYM_Graph + && graph_is_valid(to_print)) { + Expr* gfx = graph_default_graphics(to_print); + if (gfx) { graphics_show(gfx); expr_free(gfx); } + } expr_free(to_print); expr_free(parsed); @@ -392,6 +403,54 @@ static void pipe_process_input(const char* input, int id) { return; } + /* A bare valid Graph[...] auto-renders the same way as Graphics[...]: we + * swap in its default GraphPlot diagram before serializing, so `g` alone + * shows as a picture (FullForm/InputForm keep their textual heads and fall + * through to text). */ + if (evaluated->type == EXPR_FUNCTION + && evaluated->data.function.head + && evaluated->data.function.head->type == EXPR_SYMBOL + && evaluated->data.function.head->data.symbol == SYM_Graph + && graph_is_valid(evaluated)) { + Expr* gfx = graph_default_graphics(evaluated); + if (gfx) { expr_free(evaluated); evaluated = gfx; } + } + /* A bare valid Graph3D[...] auto-renders as a 3D node-link diagram. */ + if (evaluated->type == EXPR_FUNCTION + && evaluated->data.function.head + && evaluated->data.function.head->type == EXPR_SYMBOL + && evaluated->data.function.head->data.symbol == SYM_Graph3D + && graph_is_valid_head(evaluated, SYM_Graph3D)) { + Expr* gfx = graph_default_graphics3d(evaluated); + if (gfx) { expr_free(evaluated); evaluated = gfx; } + } + /* Graphics3D[...] → Plotly scatter3d scene. */ + if (evaluated->type == EXPR_FUNCTION + && evaluated->data.function.head + && evaluated->data.function.head->type == EXPR_SYMBOL + && evaluated->data.function.head->data.symbol == SYM_Graphics3D) { + char* plotly = graphics3d_to_plotly_json(evaluated); + expr_free(evaluated); + if (plotly) { + size_t json_len = strlen(plotly) + 64; + char* json_line = malloc(json_len); + if (json_line) { + strcpy(json_line, "{\"id\":"); + char id_buf[32]; snprintf(id_buf, sizeof(id_buf), "%d", id); + strcat(json_line, id_buf); + strcat(json_line, ",\"type\":\"plot\",\"payload\":"); + strcat(json_line, plotly); + strcat(json_line, "}"); + pipe_emit(json_line); + free(json_line); + } + free(plotly); + } + char done[64]; + snprintf(done, sizeof(done), "{\"id\":%d,\"type\":\"done\"}", id); + pipe_emit(done); + return; + } /* Graphics[...] → serialize as Plotly JSON for the notebook frontend. */ if (evaluated->type == EXPR_FUNCTION && evaluated->data.function.head diff --git a/src/sym_names.c b/src/sym_names.c index 6770d0f6..35126c7a 100644 --- a/src/sym_names.c +++ b/src/sym_names.c @@ -571,6 +571,19 @@ const char* SYM_DollarruSimplify = NULL; const char* SYM_Graphics = NULL; const char* SYM_Point = NULL; const char* SYM_Line = NULL; + +/* Graph subsystem (src/graph/). */ +const char* SYM_Graph = NULL; +const char* SYM_Graph3D = NULL; +const char* SYM_Graphics3D = NULL; +const char* SYM_DirectedEdge = NULL; +const char* SYM_UndirectedEdge = NULL; +const char* SYM_TwoWayRule = NULL; +const char* SYM_GraphLayout = NULL; +const char* SYM_VertexStyle = NULL; +const char* SYM_EdgeStyle = NULL; +const char* SYM_VertexLabels = NULL; +const char* SYM_VertexSize = NULL; const char* SYM_Rectangle = NULL; const char* SYM_Circle = NULL; const char* SYM_Disk = NULL; @@ -1205,6 +1218,18 @@ void sym_names_init(void) { SYM_Point = intern_symbol("Point"); SYM_Line = intern_symbol("Line"); SYM_Rectangle = intern_symbol("Rectangle"); + + SYM_Graph = intern_symbol("Graph"); + SYM_Graph3D = intern_symbol("Graph3D"); + SYM_Graphics3D = intern_symbol("Graphics3D"); + SYM_DirectedEdge = intern_symbol("DirectedEdge"); + SYM_UndirectedEdge = intern_symbol("UndirectedEdge"); + SYM_TwoWayRule = intern_symbol("TwoWayRule"); + SYM_GraphLayout = intern_symbol("GraphLayout"); + SYM_VertexStyle = intern_symbol("VertexStyle"); + SYM_EdgeStyle = intern_symbol("EdgeStyle"); + SYM_VertexLabels = intern_symbol("VertexLabels"); + SYM_VertexSize = intern_symbol("VertexSize"); SYM_Circle = intern_symbol("Circle"); SYM_Disk = intern_symbol("Disk"); SYM_Polygon = intern_symbol("Polygon"); diff --git a/src/sym_names.h b/src/sym_names.h index 22bf870f..71fd0bd6 100644 --- a/src/sym_names.h +++ b/src/sym_names.h @@ -575,6 +575,21 @@ extern const char* SYM_DollarruSimplify; extern const char* SYM_Graphics; extern const char* SYM_Point; extern const char* SYM_Line; + +/* Graph subsystem (src/graph/): Graph[]/Graph3D[] heads, edge heads and their + * parse-time sugar (Rule/TwoWayRule normalize to Directed/UndirectedEdge), and + * graph rendering options consumed by GraphPlot. */ +extern const char* SYM_Graph; +extern const char* SYM_Graph3D; +extern const char* SYM_Graphics3D; +extern const char* SYM_DirectedEdge; +extern const char* SYM_UndirectedEdge; +extern const char* SYM_TwoWayRule; +extern const char* SYM_GraphLayout; +extern const char* SYM_VertexStyle; +extern const char* SYM_EdgeStyle; +extern const char* SYM_VertexLabels; +extern const char* SYM_VertexSize; extern const char* SYM_Rectangle; extern const char* SYM_Circle; extern const char* SYM_Disk; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7a3d70f6..1fe6986b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -111,6 +111,7 @@ include_directories(../src/special_functions) include_directories(../src/numerical_calculus) include_directories(../src/numerical_roots) include_directories(../src/graphics) +include_directories(../src/graph) if(USE_ECM) include_directories(../src/external/ecm) endif() @@ -455,6 +456,109 @@ set(COMMON_SRC ../src/graphics/plot.c ../src/graphics/listplot.c ../src/graphics/show.c + ../src/graph/graph.c + ../src/graph/graph_util.c + ../src/graph/construct.c + ../src/graph/graphq.c + ../src/graph/vertexlist.c + ../src/graph/edgelist.c + ../src/graph/counts.c + ../src/graph/adjlist.c + ../src/graph/degree.c + ../src/graph/directedq.c + ../src/graph/adjmat.c + ../src/graph/incmat.c + ../src/graph/adjgraph.c + ../src/graph/generators.c + ../src/graph/shortestpath.c + ../src/graph/components.c + ../src/graph/spanningtree.c + ../src/graph/connectivity.c + ../src/graph/graphplot.c + ../src/graph/layout.c + ../src/graph/highlight.c + ../src/graph/render3d.c + ../src/graph/bipartiteq.c + ../src/graph/metrics.c + ../src/graph/acyclic.c + ../src/graph/complement.c + ../src/graph/kirchhoff.c + ../src/graph/edgeconnectivity.c + ../src/graph/linegraph.c + ../src/graph/eulerianq.c + ../src/graph/centrality.c + ../src/graph/transitiveclosure.c + ../src/graph/betweenness.c + ../src/graph/findeuler.c + ../src/graph/findhamilton.c + ../src/graph/graphpower.c + ../src/graph/findcycle.c + ../src/graph/distmatrix.c + ../src/graph/density.c + ../src/graph/degreecentrality.c + ../src/graph/findhamiltonpath.c + ../src/graph/kcore.c + ../src/graph/clustering.c + ../src/graph/globalclustering.c + ../src/graph/meanclustering.c + ../src/graph/findclique.c + ../src/graph/findindependent.c + ../src/graph/findvertexcover.c + ../src/graph/reciprocity.c + ../src/graph/chromaticpoly.c + ../src/graph/chromaticnumber.c + ../src/graph/degreesequence.c + ../src/graph/treegraphq.c + ../src/graph/stronglyconnectedq.c + ../src/graph/hamiltonianq.c + ../src/graph/regulargraphq.c + ../src/graph/completegraphq.c + ../src/graph/graphunion.c + ../src/graph/graphintersection.c + ../src/graph/graphdifference.c + ../src/graph/graphreverse.c + ../src/graph/pathgraphq.c + ../src/graph/vertexcontract.c + ../src/graph/pagerank.c + ../src/graph/katzcentrality.c + ../src/graph/graphjoin.c + ../src/graph/indexgraph.c + ../src/graph/emptygraphq.c + ../src/graph/mixedgraphq.c + ../src/graph/graphproduct.c + ../src/graph/turangraph.c + ../src/graph/completekarytree.c + ../src/graph/circulantgraph.c + ../src/graph/laddergraph.c + ../src/graph/cocktailparty.c + ../src/graph/kneser.c + ../src/graph/generalizedpetersen.c + ../src/graph/friendshipgraph.c + ../src/graph/vertexcoreness.c + ../src/graph/transitivereduction.c + ../src/graph/subgraph.c + ../src/graph/vertexdelete.c + ../src/graph/edgedelete.c + ../src/graph/edgeadd.c + ../src/graph/vertexadd.c + ../src/graph/neighborhoodgraph.c + ../src/graph/graphdisjointunion.c + ../src/graph/edgecontract.c + ../src/graph/findmatching.c + ../src/graph/finddominatingset.c + ../src/graph/findedgecover.c + ../src/graph/findvertexcoloring.c + ../src/graph/graphassortativity.c + ../src/graph/incidencelist.c + ../src/graph/vertexcomponents.c + ../src/graph/antiprismgraph.c + ../src/graph/prismgraph.c + ../src/graph/sunletgraph.c + ../src/graph/helmgraph.c + ../src/graph/geargraph.c + ../src/graph/edgebetweenness.c + ../src/graph/dodecahedralgraph.c + ../src/graph/icosahedralgraph.c ) # render.c / hershey_font.c need a live Raylib + display; only compiled @@ -1694,6 +1798,11 @@ add_executable(graphics_tests test_graphics.c ${COMMON_SRC}) target_include_directories(graphics_tests PRIVATE ../src ../src/graphics) add_test(NAME graphics_tests COMMAND graphics_tests) +add_executable(graph_tests test_graph.c ${COMMON_SRC}) +target_link_libraries(graph_tests m) +target_include_directories(graph_tests PRIVATE ../src ../src/graph) +add_test(NAME graph_tests COMMAND graph_tests) + add_executable(context_tests test_context.c ${COMMON_SRC}) target_include_directories(context_tests PRIVATE ../src) diff --git a/tests/test_graph.c b/tests/test_graph.c new file mode 100644 index 00000000..99e3a2ff --- /dev/null +++ b/tests/test_graph.c @@ -0,0 +1,1826 @@ +/* test_graph.c - Phase 1 graph subsystem tests. + * + * Covers construction/normalization (all four edge sugars), vertex derivation, + * directed-by-default, rejection of self-loops / parallel edges / 3-arg edges / + * unknown endpoints, the GraphQ predicate, terse-summary printing, and the + * InputForm round-trip through the parser. + */ + +#include "expr.h" +#include "eval.h" +#include "core.h" +#include "symtab.h" +#include "parse.h" +#include "print.h" +#include "test_utils.h" +#include + +/* ---- Normalization + FullForm (edge sugar -> canonical edges) ------------- */ +static void test_edge_sugar_normalization(void) { + /* All four accepted edge forms canonicalize to Directed/UndirectedEdge. */ + assert_eval_eq("Graph[{1,2},{1->2}]", + "Graph[List[1, 2], List[DirectedEdge[1, 2]]]", 1); + assert_eval_eq("Graph[{1,2},{DirectedEdge[1,2]}]", + "Graph[List[1, 2], List[DirectedEdge[1, 2]]]", 1); + assert_eval_eq("Graph[{1,2},{1<->2}]", + "Graph[List[1, 2], List[UndirectedEdge[1, 2]]]", 1); + assert_eval_eq("Graph[{1,2},{TwoWayRule[1,2]}]", + "Graph[List[1, 2], List[UndirectedEdge[1, 2]]]", 1); + assert_eval_eq("Graph[{1,2},{UndirectedEdge[1,2]}]", + "Graph[List[1, 2], List[UndirectedEdge[1, 2]]]", 1); +} + +/* ---- Vertex derivation (Graph[edges], directed by default) ---------------- */ +static void test_vertex_derivation(void) { + /* Vertices derived in first-appearance order; Rule defaults to directed. */ + assert_eval_eq("Graph[{1->2,2->3,3->1}]", + "Graph[List[1, 2, 3], " + "List[DirectedEdge[1, 2], DirectedEdge[2, 3], DirectedEdge[3, 1]]]", 1); + /* Derivation preserves the order endpoints first appear, not sorted. */ + assert_eval_eq("Graph[{3->1,1->2}]", + "Graph[List[3, 1, 2], " + "List[DirectedEdge[3, 1], DirectedEdge[1, 2]]]", 1); +} + +/* ---- Terse summary printing (standard form) ------------------------------- */ +static void test_summary_printing(void) { + assert_eval_eq("Graph[{1,2,3},{1->2,2->3}]", "Graph[<3 vertices, 2 edges>]", 0); + assert_eval_eq("Graph[{1,2},{1->2}]", "Graph[<2 vertices, 1 edge>]", 0); + assert_eval_eq("Graph[{1},{}]", "Graph[<1 vertex, 0 edges>]", 0); +} + +/* ---- GraphQ truth table --------------------------------------------------- */ +static void test_graphq(void) { + assert_eval_eq("GraphQ[Graph[{1,2},{1->2}]]", "True", 0); + assert_eval_eq("GraphQ[Graph[{1,2},{1<->2}]]", "True", 0); + assert_eval_eq("GraphQ[Graph[{1,2,3},{1->2,2->3}]]", "True", 0); + /* Not a graph at all. */ + assert_eval_eq("GraphQ[5]", "False", 0); + assert_eval_eq("GraphQ[foo]", "False", 0); +} + +/* ---- Rejection of malformed graphs (stay unevaluated -> GraphQ False) ------ */ +static void test_rejections(void) { + /* Self-loop. */ + assert_eval_eq("GraphQ[Graph[{1},{1->1}]]", "False", 0); + /* Parallel/duplicate edges. */ + assert_eval_eq("GraphQ[Graph[{1,2},{1->2,1->2}]]", "False", 0); + assert_eval_eq("GraphQ[Graph[{1,2},{1<->2,2<->1}]]", "False", 0); + /* 3-argument edge (reserved for future edge tags). */ + assert_eval_eq("GraphQ[Graph[{1,2},{DirectedEdge[1,2,x]}]]", "False", 0); + /* Edge endpoint absent from an explicit vertex list. */ + assert_eval_eq("GraphQ[Graph[{1,2},{1->3}]]", "False", 0); + /* Anti-parallel *directed* edges are allowed (distinct). */ + assert_eval_eq("GraphQ[Graph[{1,2},{1->2,2->1}]]", "True", 0); +} + +/* ---- InputForm round-trips through the parser ----------------------------- */ +static void test_inputform_roundtrip(void) { + static const char* inputs[] = { + "Graph[{1,2},{1<->2}]", + "Graph[{1,2,3},{1->2,2->3,3->1}]", + "Graph[{a,b,c},{a<->b,b->c}]", + }; + for (size_t i = 0; i < sizeof(inputs) / sizeof(inputs[0]); i++) { + Expr* g = evaluate(parse_expression(inputs[i])); + ASSERT(g != NULL); + /* Wrap in InputForm and print: yields the literal constructor. */ + Expr* wrap_args[1] = { expr_copy(g) }; + Expr* wrap = expr_new_function(expr_new_symbol("InputForm"), wrap_args, 1); + char* s = expr_to_string(wrap); + /* Re-parse and evaluate: must reproduce an equal graph. */ + Expr* g2 = evaluate(parse_expression(s)); + ASSERT(expr_eq(g, g2)); + free(s); + expr_free(wrap); + expr_free(g); + expr_free(g2); + } + /* Spot-check the exact InputForm text for the undirected case. */ + assert_eval_eq("InputForm[Graph[{1,2},{1<->2}]]", "Graph[{1, 2}, {1 <-> 2}]", 0); + assert_eval_eq("InputForm[Graph[{1,2},{1->2}]]", "Graph[{1, 2}, {1 -> 2}]", 0); +} + +/* ---- Phase 2: query / representation builtins ----------------------------- */ +static void test_query_builtins(void) { + /* g = 1->2->3->4->1 (directed cycle). */ + const char* g = "Graph[{1,2,3,4},{1->2,2->3,3->4,4->1}]"; + char buf[256]; + + snprintf(buf, sizeof(buf), "VertexList[%s]", g); + assert_eval_eq(buf, "{1, 2, 3, 4}", 0); + snprintf(buf, sizeof(buf), "EdgeList[%s]", g); + assert_eval_eq(buf, "{1 -> 2, 2 -> 3, 3 -> 4, 4 -> 1}", 0); + snprintf(buf, sizeof(buf), "VertexCount[%s]", g); + assert_eval_eq(buf, "4", 0); + snprintf(buf, sizeof(buf), "EdgeCount[%s]", g); + assert_eval_eq(buf, "4", 0); + + /* Directed cycle: every vertex has in=out=1, total degree 2. */ + snprintf(buf, sizeof(buf), "VertexDegree[%s, 1]", g); + assert_eval_eq(buf, "2", 0); + snprintf(buf, sizeof(buf), "VertexInDegree[%s, 1]", g); + assert_eval_eq(buf, "1", 0); + snprintf(buf, sizeof(buf), "VertexOutDegree[%s, 1]", g); + assert_eval_eq(buf, "1", 0); + snprintf(buf, sizeof(buf), "VertexDegree[%s]", g); + assert_eval_eq(buf, "{2, 2, 2, 2}", 0); + + /* Successor adjacency for a directed graph. */ + snprintf(buf, sizeof(buf), "AdjacencyList[%s, 1]", g); + assert_eval_eq(buf, "{2}", 0); + snprintf(buf, sizeof(buf), "AdjacencyList[%s]", g); + assert_eval_eq(buf, "{{2}, {3}, {4}, {1}}", 0); + + snprintf(buf, sizeof(buf), "DirectedGraphQ[%s]", g); + assert_eval_eq(buf, "True", 0); +} + +static void test_query_undirected(void) { + /* Path 1 <-> 2 <-> 3 (undirected). */ + const char* g = "Graph[{1,2,3},{1<->2,2<->3}]"; + char buf[256]; + + /* Undirected: middle vertex has degree 2, ends degree 1. */ + snprintf(buf, sizeof(buf), "VertexDegree[%s]", g); + assert_eval_eq(buf, "{1, 2, 1}", 0); + /* Undirected neighbors go both ways. */ + snprintf(buf, sizeof(buf), "AdjacencyList[%s, 2]", g); + assert_eval_eq(buf, "{1, 3}", 0); + snprintf(buf, sizeof(buf), "DirectedGraphQ[%s]", g); + assert_eval_eq(buf, "False", 0); + /* In/out degree equal the degree for undirected graphs. */ + snprintf(buf, sizeof(buf), "VertexInDegree[%s, 2]", g); + assert_eval_eq(buf, "2", 0); + snprintf(buf, sizeof(buf), "VertexOutDegree[%s, 2]", g); + assert_eval_eq(buf, "2", 0); +} + +/* ---- Phase 3: matrix views ------------------------------------------------ */ +static void test_matrix_views(void) { + /* Directed 4-cycle: circulant adjacency matrix. */ + const char* dg = "Graph[{1,2,3,4},{1->2,2->3,3->4,4->1}]"; + char buf[256]; + snprintf(buf, sizeof(buf), "AdjacencyMatrix[%s]", dg); + assert_eval_eq(buf, + "{{0, 1, 0, 0}, {0, 0, 1, 0}, {0, 0, 0, 1}, {1, 0, 0, 0}}", 0); + + /* Undirected edge -> symmetric matrix. */ + assert_eval_eq("AdjacencyMatrix[Graph[{1,2},{1<->2}]]", + "{{0, 1}, {1, 0}}", 0); + + /* Feeds linalg unchanged: trace of the 4-cycle adjacency is 0. */ + snprintf(buf, sizeof(buf), "Tr[AdjacencyMatrix[%s]]", dg); + assert_eval_eq(buf, "0", 0); + /* Det of the directed 4-cycle circulant is -1. */ + snprintf(buf, sizeof(buf), "Det[AdjacencyMatrix[%s]]", dg); + assert_eval_eq(buf, "-1", 0); + + /* Round-trip: AdjacencyGraph[AdjacencyMatrix[g]] reproduces the edges. */ + snprintf(buf, sizeof(buf), "EdgeList[AdjacencyGraph[AdjacencyMatrix[%s]]]", dg); + assert_eval_eq(buf, "{1 -> 2, 2 -> 3, 3 -> 4, 4 -> 1}", 0); + /* Undirected round-trip stays undirected. */ + assert_eval_eq( + "EdgeList[AdjacencyGraph[AdjacencyMatrix[Graph[{1,2,3},{1<->2,2<->3}]]]]", + "{1 <-> 2, 2 <-> 3}", 0); + + /* Incidence matrix of an undirected path 1<->2<->3. */ + assert_eval_eq("IncidenceMatrix[Graph[{1,2,3},{1<->2,2<->3}]]", + "{{1, 0}, {1, 1}, {0, 1}}", 0); +} + +/* ---- Phase 4: generators -------------------------------------------------- */ +static void test_generators(void) { + /* K5: 5 vertices, 10 edges, undirected, every vertex degree 4. */ + assert_eval_eq("VertexCount[CompleteGraph[5]]", "5", 0); + assert_eval_eq("EdgeCount[CompleteGraph[5]]", "10", 0); + assert_eval_eq("DirectedGraphQ[CompleteGraph[5]]", "False", 0); + assert_eval_eq("VertexDegree[CompleteGraph[5]]", "{4, 4, 4, 4, 4}", 0); + + /* CycleGraph[n]: n vertices, n edges, every degree 2. */ + assert_eval_eq("EdgeCount[CycleGraph[5]]", "5", 0); + assert_eval_eq("VertexDegree[CycleGraph[5]]", "{2, 2, 2, 2, 2}", 0); + assert_eval_eq("EdgeList[CycleGraph[4]]", + "{1 <-> 2, 2 <-> 3, 3 <-> 4, 4 <-> 1}", 0); + + /* PathGraph[n]: n vertices, n-1 edges; endpoints degree 1. */ + assert_eval_eq("EdgeCount[PathGraph[5]]", "4", 0); + assert_eval_eq("VertexDegree[PathGraph[5]]", "{1, 2, 2, 2, 1}", 0); + /* Explicit-vertex path. */ + assert_eval_eq("EdgeList[PathGraph[{a,b,c}]]", "{a <-> b, b <-> c}", 0); +} + +static void test_random_graph(void) { + /* Exactly m distinct edges, n vertices, valid simple graph. */ + assert_eval_eq("VertexCount[RandomGraph[{6, 5}]]", "6", 0); + assert_eval_eq("EdgeCount[RandomGraph[{6, 5}]]", "5", 0); + assert_eval_eq("GraphQ[RandomGraph[{6, 5}]]", "True", 0); + /* Too many edges for a simple graph -> unevaluated (GraphQ False). */ + assert_eval_eq("GraphQ[RandomGraph[{3, 10}]]", "False", 0); + /* Determinism under a fixed seed. */ + Expr* e1 = evaluate(parse_expression( + "(SeedRandom[42]; EdgeList[RandomGraph[{6,5}]]) === " + "(SeedRandom[42]; EdgeList[RandomGraph[{6,5}]])")); + char* s = expr_to_string(e1); + ASSERT(strcmp(s, "True") == 0); + free(s); + expr_free(e1); +} + +/* ---- Phase 5: algorithms -------------------------------------------------- */ +static void test_shortest_path(void) { + /* Directed path 1->2->3->4. */ + const char* dg = "Graph[{1,2,3,4},{1->2,2->3,3->4}]"; + char buf[256]; + snprintf(buf, sizeof(buf), "FindShortestPath[%s, 1, 4]", dg); + assert_eval_eq(buf, "{1, 2, 3, 4}", 0); + snprintf(buf, sizeof(buf), "GraphDistance[%s, 1, 4]", dg); + assert_eval_eq(buf, "3", 0); + /* Direction matters: 4 cannot reach 1. */ + snprintf(buf, sizeof(buf), "GraphDistance[%s, 4, 1]", dg); + assert_eval_eq(buf, "Infinity", 0); + snprintf(buf, sizeof(buf), "FindShortestPath[%s, 4, 1]", dg); + assert_eval_eq(buf, "{}", 0); + /* Undirected: reachable both ways. */ + assert_eval_eq("GraphDistance[Graph[{1,2,3,4},{1<->2,2<->3,3<->4}], 4, 1]", "3", 0); + /* A shortcut shortens the path. */ + assert_eval_eq( + "GraphDistance[Graph[{1,2,3,4},{1->2,2->3,3->4,1->4}], 1, 4]", "1", 0); +} + +static void test_components(void) { + /* Directed 4-cycle: one weak and one strong component. */ + assert_eval_eq("ConnectedComponents[Graph[{1,2,3,4},{1->2,2->3,3->4,4->1}]]", + "{{1, 2, 3, 4}}", 0); + /* Two disjoint directed pieces -> two weak components. */ + assert_eval_eq("ConnectedComponents[Graph[{1,2,3,4},{1->2,3->4}]]", + "{{1, 2}, {3, 4}}", 0); + /* Directed chain 1->2->3: weak = all together, strong = singletons. */ + assert_eval_eq("WeaklyConnectedComponents[Graph[{1,2,3},{1->2,2->3}]]", + "{{1, 2, 3}}", 0); + assert_eval_eq("StronglyConnectedComponents[Graph[{1,2,3},{1->2,2->3}]]", + "{{1}, {2}, {3}}", 0); + /* A directed cycle is one strong component. */ + assert_eval_eq("StronglyConnectedComponents[Graph[{1,2,3},{1->2,2->3,3->1}]]", + "{{1, 2, 3}}", 0); +} + +static void test_spanning_and_connectivity(void) { + /* Spanning tree of a connected graph has VertexCount - 1 edges. */ + assert_eval_eq("EdgeCount[FindSpanningTree[CompleteGraph[5]]]", "4", 0); + assert_eval_eq("EdgeCount[FindSpanningTree[CycleGraph[6]]]", "5", 0); + + /* Connectivity. */ + assert_eval_eq("ConnectedGraphQ[PathGraph[4]]", "True", 0); + assert_eval_eq("ConnectedGraphQ[Graph[{1,2,3,4},{1<->2,3<->4}]]", "False", 0); + /* kappa: path=1, cycle=2, complete K4=3, disconnected=0. */ + assert_eval_eq("VertexConnectivity[PathGraph[4]]", "1", 0); + assert_eval_eq("VertexConnectivity[CycleGraph[5]]", "2", 0); + assert_eval_eq("VertexConnectivity[CompleteGraph[4]]", "3", 0); + assert_eval_eq("VertexConnectivity[Graph[{1,2,3,4},{1<->2,3<->4}]]", "0", 0); +} + +/* ---- Phase 6: GraphPlot --------------------------------------------------- */ +static void test_graphplot(void) { + /* Emits a Graphics object. */ + assert_eval_eq("Head[GraphPlot[CycleGraph[5]]]", "Graphics", 0); + /* One Line per edge, one Disk per vertex. */ + assert_eval_eq("Count[GraphPlot[CycleGraph[5]], _Line, Infinity]", "5", 0); + assert_eval_eq("Count[GraphPlot[CycleGraph[5]], _Disk, Infinity]", "5", 0); + /* CompleteGraph[6]: 15 edges, 6 vertices. */ + assert_eval_eq("Count[GraphPlot[CompleteGraph[6]], _Line, Infinity]", "15", 0); + assert_eval_eq("Count[GraphPlot[CompleteGraph[6]], _Disk, Infinity]", "6", 0); + /* Non-graph argument stays unevaluated. */ + assert_eval_eq("Head[GraphPlot[5]]", "GraphPlot", 0); +} + +static void test_graphplot_options(void) { + /* Every layout still yields a Graphics with the same edge/vertex counts; + * only the coordinates differ. */ + assert_eval_eq("Head[GraphPlot[CompleteGraph[6], GraphLayout -> \"SpringElectricalEmbedding\"]]", "Graphics", 0); + assert_eval_eq("Count[GraphPlot[CompleteGraph[6], GraphLayout -> \"SpringElectricalEmbedding\"], _Line, Infinity]", "15", 0); + assert_eval_eq("Count[GraphPlot[CycleGraph[8], GraphLayout -> \"GridEmbedding\"], _Disk, Infinity]", "8", 0); + /* Unknown layout name falls back to circular (still valid). */ + assert_eval_eq("Head[GraphPlot[CycleGraph[5], GraphLayout -> \"NoSuchEmbedding\"]]", "Graphics", 0); + /* VertexLabels -> None suppresses the Text labels; default draws them. */ + assert_eval_eq("Count[GraphPlot[CycleGraph[5], VertexLabels -> None], _Text, Infinity]", "0", 0); + assert_eval_eq("Count[GraphPlot[CycleGraph[5]], _Text, Infinity]", "5", 0); + /* Styling emits per-primitive RGBColor directives (one per edge + vertex). */ + assert_eval_eq("Count[GraphPlot[CycleGraph[5]], _RGBColor, Infinity]", "10", 0); +} + +static void test_highlight_graph(void) { + /* HighlightGraph returns a Graphics (not a Graph). */ + assert_eval_eq("Head[HighlightGraph[CycleGraph[8], {1, 2, 3}]]", "Graphics", 0); + assert_eval_eq("Head[HighlightGraph[CompleteGraph[6], {1 <-> 2, 2 <-> 3}]]", "Graphics", 0); + /* Edge/vertex counts are preserved (highlighting only recolors). */ + assert_eval_eq("Count[HighlightGraph[CycleGraph[6], {1, 2}], _Line, Infinity]", "6", 0); + assert_eval_eq("Count[HighlightGraph[CycleGraph[6], {1, 2}], _Disk, Infinity]", "6", 0); + /* A vertex list is treated as a path (accepted, still a Graphics). */ + assert_eval_eq("Head[HighlightGraph[PathGraph[5], {{1, 2, 3}}]]", "Graphics", 0); + /* Non-graph / malformed arg stays unevaluated. */ + assert_eval_eq("Head[HighlightGraph[5, {1}]]", "HighlightGraph", 0); +} + +static void test_graph3d(void) { + /* Graph3D builds a canonical value with head Graph3D, and counts as a graph. */ + assert_eval_eq("GraphQ[Graph3D[{1,2},{1->2}]]", "True", 0); + assert_eval_eq("Head[Graph3D[{1,2,3},{1<->2,2<->3}]]", "Graph3D", 0); + /* Same vertex/edge readers work on a Graph3D value. */ + assert_eval_eq("VertexCount[Graph3D[{1,2,3},{1<->2,2<->3}]]", "3", 0); + assert_eval_eq("EdgeCount[Graph3D[{1,2,3},{1<->2,2<->3}]]", "2", 0); + /* Graph3D of an existing graph reuses its vertices/edges. */ + assert_eval_eq("VertexCount[Graph3D[CompleteGraph[5]]]", "5", 0); + assert_eval_eq("EdgeCount[Graph3D[CompleteGraph[5]]]", "10", 0); + /* InputForm round-trips the constructor. */ + assert_eval_eq("InputForm[Graph3D[{1,2},{1<->2}]]", "Graph3D[{1, 2}, {1 <-> 2}]", 0); + /* Malformed (self-loop) stays unevaluated. */ + assert_eval_eq("Head[Graph3D[{1},{1->1}]]", "Graph3D", 0); +} + +static void test_bipartite(void) { + /* Even cycles / paths / stars / complete bipartite are 2-colorable. */ + assert_eval_eq("BipartiteGraphQ[CycleGraph[4]]", "True", 0); + assert_eval_eq("BipartiteGraphQ[PathGraph[5]]", "True", 0); + assert_eval_eq("BipartiteGraphQ[Graph[{0,1,2,3},{0<->1,0<->2,0<->3}]]", "True", 0); + /* Odd cycles / triangles are not. */ + assert_eval_eq("BipartiteGraphQ[CycleGraph[5]]", "False", 0); + assert_eval_eq("BipartiteGraphQ[CompleteGraph[3]]", "False", 0); + /* Direction is ignored; edgeless is vacuously bipartite. */ + assert_eval_eq("BipartiteGraphQ[Graph[{1,2,3,4},{1->2,2->3,3->4,4->1}]]", "True", 0); + assert_eval_eq("BipartiteGraphQ[Graph[{1,2,3},{}]]", "True", 0); + /* One non-bipartite component makes the whole graph non-bipartite. */ + assert_eval_eq("BipartiteGraphQ[Graph[{1,2,3,4,5},{1<->2,2<->3,3<->1,4<->5}]]", "False", 0); + /* Non-graph argument. */ + assert_eval_eq("BipartiteGraphQ[5]", "False", 0); +} + +static void test_metrics(void) { + /* Path P5: eccentricities {4,3,2,3,4}; diameter 4, radius 2, center {3}. */ + assert_eval_eq("VertexEccentricity[PathGraph[5]]", "{4, 3, 2, 3, 4}", 0); + assert_eval_eq("GraphDiameter[PathGraph[5]]", "4", 0); + assert_eval_eq("GraphRadius[PathGraph[5]]", "2", 0); + assert_eval_eq("GraphCenter[PathGraph[5]]", "{3}", 0); + /* Cycle C6: every eccentricity 3; center is all vertices. */ + assert_eval_eq("GraphDiameter[CycleGraph[6]]", "3", 0); + assert_eval_eq("GraphCenter[CycleGraph[6]]", "{1, 2, 3, 4, 5, 6}", 0); + /* Complete graph: diameter 1. */ + assert_eval_eq("GraphDiameter[CompleteGraph[5]]", "1", 0); + /* Disconnected: diameter/radius Infinity, empty center. */ + assert_eval_eq("GraphDiameter[Graph[{1,2,3},{1<->2}]]", "Infinity", 0); + assert_eval_eq("GraphRadius[Graph[{1,2,3},{1<->2}]]", "Infinity", 0); + assert_eval_eq("GraphCenter[Graph[{1,2,3},{1<->2}]]", "{}", 0); + /* Directed out-star: source reaches all (ecc 1), leaves unreachable → + diameter Infinity but radius 1, center is the source. */ + assert_eval_eq("GraphRadius[Graph[{0,1,2,3},{0->1,0->2,0->3}]]", "1", 0); + assert_eval_eq("GraphCenter[Graph[{0,1,2,3},{0->1,0->2,0->3}]]", "{0}", 0); + assert_eval_eq("VertexEccentricity[Graph[{1},{}], 1]", "0", 0); + /* Non-graph stays unevaluated. */ + assert_eval_eq("Head[GraphDiameter[5]]", "GraphDiameter", 0); +} + +static void test_acyclic(void) { + /* Directed DAG: a valid order + acyclic. */ + assert_eval_eq("TopologicalSort[Graph[{1,2,3},{1->2,2->3,1->3}]]", "{1, 2, 3}", 0); + assert_eval_eq("AcyclicGraphQ[Graph[{1,2,3},{1->2,2->3,1->3}]]", "True", 0); + /* Directed cycle: $Failed + not acyclic. */ + assert_eval_eq("TopologicalSort[Graph[{1,2,3},{1->2,2->3,3->1}]]", "$Failed", 0); + assert_eval_eq("AcyclicGraphQ[Graph[{1,2,3},{1->2,2->3,3->1}]]", "False", 0); + assert_eval_eq("AcyclicGraphQ[Graph[{1,2},{1->2,2->1}]]", "False", 0); + /* Undirected forest is acyclic; an undirected cycle is not. Topological + sort is $Failed for undirected edges (they act as 2-cycles). */ + assert_eval_eq("AcyclicGraphQ[PathGraph[4]]", "True", 0); + assert_eval_eq("AcyclicGraphQ[Graph[{1,2,3,4,5},{1<->2,2<->3,4<->5}]]", "True", 0); + assert_eval_eq("AcyclicGraphQ[CycleGraph[4]]", "False", 0); + assert_eval_eq("TopologicalSort[PathGraph[4]]", "$Failed", 0); + /* Edgeless graph: acyclic, order is all vertices. */ + assert_eval_eq("AcyclicGraphQ[Graph[{1,2,3},{}]]", "True", 0); + assert_eval_eq("TopologicalSort[Graph[{1,2,3},{}]]", "{1, 2, 3}", 0); + /* Non-graph. */ + assert_eval_eq("AcyclicGraphQ[5]", "False", 0); +} + +static void test_complement(void) { + /* Complement of a path keeps the one non-edge; complement of empty is K_n; + complement of complete is edgeless. */ + assert_eval_eq("EdgeList[GraphComplement[PathGraph[3]]]", "{1 <-> 3}", 0); + assert_eval_eq("EdgeCount[GraphComplement[Graph[{1,2,3},{}]]]", "3", 0); + assert_eval_eq("EdgeCount[GraphComplement[CompleteGraph[4]]]", "0", 0); + assert_eval_eq("EdgeCount[GraphComplement[CycleGraph[4]]]", "2", 0); + /* Double complement restores the edge count. */ + assert_eval_eq("EdgeCount[GraphComplement[GraphComplement[CycleGraph[5]]]]", "5", 0); + /* Directed complement stays directed (all ordered non-self pairs minus 1->2). */ + assert_eval_eq("EdgeCount[GraphComplement[Graph[{1,2,3},{1->2}]]]", "5", 0); + assert_eval_eq("DirectedGraphQ[GraphComplement[Graph[{1,2,3},{1->2}]]]", "True", 0); + assert_eval_eq("GraphQ[GraphComplement[PathGraph[4]]]", "True", 0); +} + +static void test_kirchhoff(void) { + /* Laplacian = D - A, exact small cases. */ + assert_eval_eq("KirchhoffMatrix[PathGraph[3]]", "{{1, -1, 0}, {-1, 2, -1}, {0, -1, 1}}", 0); + assert_eval_eq("KirchhoffMatrix[CycleGraph[3]]", "{{2, -1, -1}, {-1, 2, -1}, {-1, -1, 2}}", 0); + /* Every row sums to 0; trace = sum of degrees = 2|E|. */ + assert_eval_eq("Total[KirchhoffMatrix[CycleGraph[4]]]", "{0, 0, 0, 0}", 0); + assert_eval_eq("Tr[KirchhoffMatrix[CycleGraph[4]]]", "8", 0); + /* Interop: Laplacian eigenvalues include 0 (once, since connected). */ + assert_eval_eq("Eigenvalues[KirchhoffMatrix[PathGraph[3]]]", "{3, 1, 0}", 0); + assert_eval_eq("Head[KirchhoffMatrix[5]]", "KirchhoffMatrix", 0); +} + +static void test_edge_connectivity(void) { + /* Complete K_n -> n-1; cycle -> 2; path/tree/star -> 1. */ + assert_eval_eq("EdgeConnectivity[CompleteGraph[4]]", "3", 0); + assert_eval_eq("EdgeConnectivity[CycleGraph[5]]", "2", 0); + assert_eval_eq("EdgeConnectivity[PathGraph[4]]", "1", 0); + assert_eval_eq("EdgeConnectivity[Graph[{0,1,2,3},{0<->1,0<->2,0<->3}]]", "1", 0); + /* A bridge between two triangles -> 1. */ + assert_eval_eq("EdgeConnectivity[Graph[{1,2,3,4,5,6},{1<->2,2<->3,3<->1,4<->5,5<->6,6<->4,3<->4}]]", "1", 0); + /* Disconnected -> 0. */ + assert_eval_eq("EdgeConnectivity[Graph[{1,2,3},{1<->2}]]", "0", 0); + /* Directed: strongly connected cycle -> 1; a path is not, -> 0. */ + assert_eval_eq("EdgeConnectivity[Graph[{1,2,3},{1->2,2->3,3->1}]]", "1", 0); + assert_eval_eq("EdgeConnectivity[Graph[{1,2,3},{1->2,2->3}]]", "0", 0); + assert_eval_eq("Head[EdgeConnectivity[5]]", "EdgeConnectivity", 0); +} + +static void test_line_graph(void) { + /* L(P3): the two edges share vertex 2 → one edge; vertices ARE the edges. */ + assert_eval_eq("VertexCount[LineGraph[PathGraph[3]]]", "2", 0); + assert_eval_eq("EdgeCount[LineGraph[PathGraph[3]]]", "1", 0); + assert_eval_eq("VertexList[LineGraph[PathGraph[3]]]", "{1 <-> 2, 2 <-> 3}", 0); + /* L(C3) = K3; L(Cn) = Cn; L(K4) has 6 vertices and 12 edges. */ + assert_eval_eq("EdgeCount[LineGraph[CycleGraph[3]]]", "3", 0); + assert_eval_eq("EdgeCount[LineGraph[CycleGraph[5]]]", "5", 0); + assert_eval_eq("VertexCount[LineGraph[CompleteGraph[4]]]", "6", 0); + assert_eval_eq("EdgeCount[LineGraph[CompleteGraph[4]]]", "12", 0); + /* Star K1,3 → the three edges pairwise share the center → K3. */ + assert_eval_eq("EdgeCount[LineGraph[Graph[{0,1,2,3},{0<->1,0<->2,0<->3}]]]", "3", 0); + /* Directed line graph joins arcs head-to-tail and stays directed. */ + assert_eval_eq("EdgeCount[LineGraph[Graph[{1,2,3},{1->2,2->3}]]]", "1", 0); + assert_eval_eq("DirectedGraphQ[LineGraph[Graph[{1,2,3},{1->2,2->3}]]]", "True", 0); +} + +static void test_eulerian(void) { + /* Even + connected → Eulerian; odd degree → not. */ + assert_eval_eq("EulerianGraphQ[CycleGraph[5]]", "True", 0); + assert_eval_eq("EulerianGraphQ[PathGraph[4]]", "False", 0); + assert_eval_eq("EulerianGraphQ[CompleteGraph[3]]", "True", 0); + assert_eval_eq("EulerianGraphQ[CompleteGraph[4]]", "False", 0); + assert_eval_eq("EulerianGraphQ[CompleteGraph[5]]", "True", 0); + /* Figure-eight (two triangles sharing a vertex) is Eulerian; two disjoint + cycles are not (disconnected). An isolated vertex is ignored. */ + assert_eval_eq("EulerianGraphQ[Graph[{1,2,3,4,5},{1<->2,2<->3,3<->1,1<->4,4<->5,5<->1}]]", "True", 0); + assert_eval_eq("EulerianGraphQ[Graph[{1,2,3,4,5,6},{1<->2,2<->3,3<->1,4<->5,5<->6,6<->4}]]", "False", 0); + assert_eval_eq("EulerianGraphQ[Graph[{1,2,3,4},{1<->2,2<->3,3<->1}]]", "True", 0); + assert_eval_eq("EulerianGraphQ[Graph[{1,2,3},{}]]", "True", 0); + /* Directed: in==out everywhere and connected → Eulerian; a path is not. */ + assert_eval_eq("EulerianGraphQ[Graph[{1,2,3},{1->2,2->3,3->1}]]", "True", 0); + assert_eval_eq("EulerianGraphQ[Graph[{1,2,3},{1->2,2->3}]]", "False", 0); + assert_eval_eq("EulerianGraphQ[5]", "False", 0); +} + +static void test_star_wheel(void) { + /* Star K_{1,n-1}: center joined to n-1 leaves; bipartite. */ + assert_eval_eq("VertexCount[StarGraph[5]]", "5", 0); + assert_eval_eq("EdgeCount[StarGraph[5]]", "4", 0); + assert_eval_eq("VertexDegree[StarGraph[5], 1]", "4", 0); + assert_eval_eq("BipartiteGraphQ[StarGraph[5]]", "True", 0); + assert_eval_eq("EdgeList[StarGraph[4]]", "{1 <-> 2, 1 <-> 3, 1 <-> 4}", 0); + /* Wheel: rim cycle + hub; 2(n-1) edges, hub degree n-1, has triangles so + it is not bipartite; W_4 = K_4. Small n stays unevaluated. */ + assert_eval_eq("VertexCount[WheelGraph[5]]", "5", 0); + assert_eval_eq("EdgeCount[WheelGraph[5]]", "8", 0); + assert_eval_eq("VertexDegree[WheelGraph[5], 5]", "4", 0); + assert_eval_eq("BipartiteGraphQ[WheelGraph[5]]", "False", 0); + assert_eval_eq("EdgeCount[WheelGraph[4]]", "6", 0); + assert_eval_eq("Head[WheelGraph[3]]", "WheelGraph", 0); +} + +static void test_complete_multipartite(void) { + /* CompleteGraph[{m,n}] is complete bipartite K_{m,n}. */ + assert_eval_eq("VertexCount[CompleteGraph[{2,3}]]", "5", 0); + assert_eval_eq("EdgeCount[CompleteGraph[{2,3}]]", "6", 0); + assert_eval_eq("BipartiteGraphQ[CompleteGraph[{2,3}]]", "True", 0); + assert_eval_eq("EdgeCount[CompleteGraph[{3,3}]]", "9", 0); + assert_eval_eq("VertexDegree[CompleteGraph[{2,3}], 1]", "3", 0); + /* Complete multipartite: K_{2,2,2} is the octahedron (12 edges, not bip.). */ + assert_eval_eq("EdgeCount[CompleteGraph[{2,2,2}]]", "12", 0); + assert_eval_eq("BipartiteGraphQ[CompleteGraph[{2,2,2}]]", "False", 0); + /* The integer form is unchanged. */ + assert_eval_eq("EdgeCount[CompleteGraph[5]]", "10", 0); +} + +static void test_grid_hypercube(void) { + /* Grid: 2x3 has 7 edges, k-dim works, {n} is a path, and it is bipartite. */ + assert_eval_eq("VertexCount[GridGraph[{2,3}]]", "6", 0); + assert_eval_eq("EdgeCount[GridGraph[{2,3}]]", "7", 0); + assert_eval_eq("EdgeCount[GridGraph[{3,3}]]", "12", 0); + assert_eval_eq("BipartiteGraphQ[GridGraph[{3,3}]]", "True", 0); + assert_eval_eq("EdgeCount[GridGraph[{4}]]", "3", 0); + assert_eval_eq("Head[GridGraph[5]]", "GridGraph", 0); + /* Hypercube Q_k: 2^k vertices, k*2^(k-1) edges, k-regular, bipartite; + Q_2 = C_4; the 2x2x2 grid is Q_3. */ + assert_eval_eq("VertexCount[HypercubeGraph[3]]", "8", 0); + assert_eval_eq("EdgeCount[HypercubeGraph[3]]", "12", 0); + assert_eval_eq("BipartiteGraphQ[HypercubeGraph[3]]", "True", 0); + assert_eval_eq("VertexDegree[HypercubeGraph[3], 1]", "3", 0); + assert_eval_eq("EdgeCount[HypercubeGraph[2]]", "4", 0); + assert_eval_eq("VertexCount[HypercubeGraph[0]]", "1", 0); + assert_eval_eq("EdgeCount[GridGraph[{2,2,2}]]", "12", 0); +} + +static void test_closeness(void) { + /* Exact rationals: complete → all 1; cycle C4 → all 3/4; path P3 → ends 2/3, + middle 1; star center 1, leaves 3/5. */ + assert_eval_eq("ClosenessCentrality[CompleteGraph[4]]", "{1, 1, 1, 1}", 0); + assert_eval_eq("ClosenessCentrality[CycleGraph[4]]", "{3/4, 3/4, 3/4, 3/4}", 0); + assert_eval_eq("ClosenessCentrality[PathGraph[3]]", "{2/3, 1, 2/3}", 0); + assert_eval_eq("ClosenessCentrality[StarGraph[4]]", "{1, 3/5, 3/5, 3/5}", 0); + /* Isolated / disconnected vertices score 0. */ + assert_eval_eq("ClosenessCentrality[Graph[{1,2},{}]]", "{0, 0}", 0); + /* Directed: distances follow direction; a sink reaches nobody → 0. */ + assert_eval_eq("ClosenessCentrality[Graph[{1,2,3},{1->2,2->3}]]", "{2/3, 1/2, 0}", 0); + assert_eval_eq("Head[ClosenessCentrality[5]]", "ClosenessCentrality", 0); +} + +static void test_transitive_closure(void) { + /* Directed chain: closure adds the reachable 1->3; stays directed. */ + assert_eval_eq("EdgeList[TransitiveClosure[Graph[{1,2,3},{1->2,2->3}]]]", "{1 -> 2, 1 -> 3, 2 -> 3}", 0); + assert_eval_eq("DirectedGraphQ[TransitiveClosure[Graph[{1,2,3},{1->2,2->3}]]]", "True", 0); + /* Directed cycle: everyone reaches everyone → complete digraph (6 arcs). */ + assert_eval_eq("EdgeCount[TransitiveClosure[Graph[{1,2,3},{1->2,2->3,3->1}]]]", "6", 0); + /* Undirected: each component becomes a clique. */ + assert_eval_eq("EdgeList[TransitiveClosure[PathGraph[3]]]", "{1 <-> 2, 1 <-> 3, 2 <-> 3}", 0); + assert_eval_eq("EdgeCount[TransitiveClosure[CycleGraph[4]]]", "6", 0); + assert_eval_eq("EdgeCount[TransitiveClosure[Graph[{1,2,3,4,5},{1<->2,3<->4,4<->5}]]]", "4", 0); + assert_eval_eq("EdgeCount[TransitiveClosure[CompleteGraph[4]]]", "6", 0); + assert_eval_eq("Head[TransitiveClosure[5]]", "TransitiveClosure", 0); +} + +static void test_betweenness(void) { + /* Path: internal vertex k has (k-1)(n-k); complete has all 0; star center + carries every leaf-leaf path. */ + assert_eval_eq("BetweennessCentrality[PathGraph[3]]", "{0, 1, 0}", 0); + assert_eval_eq("BetweennessCentrality[PathGraph[5]]", "{0, 3, 4, 3, 0}", 0); + assert_eval_eq("BetweennessCentrality[CompleteGraph[4]]", "{0, 0, 0, 0}", 0); + assert_eval_eq("BetweennessCentrality[StarGraph[5]]", "{6, 0, 0, 0, 0}", 0); + /* Tied shortest paths give exact fractions: C4 antipodal pairs → 1/2 each. */ + assert_eval_eq("BetweennessCentrality[CycleGraph[4]]", "{1/2, 1/2, 1/2, 1/2}", 0); + /* Directed counts ordered pairs (no halving). */ + assert_eval_eq("BetweennessCentrality[Graph[{1,2,3},{1->2,2->3}]]", "{0, 1, 0}", 0); + assert_eval_eq("BetweennessCentrality[Graph[{1,2,3},{}]]", "{0, 0, 0}", 0); + assert_eval_eq("Head[BetweennessCentrality[5]]", "BetweennessCentrality", 0); +} + +static void test_find_eulerian(void) { + /* Eulerian graphs return a closed tour of ne+1 vertices. */ + assert_eval_eq("FindEulerianCycle[CycleGraph[3]]", "{1, 2, 3, 1}", 0); + assert_eval_eq("Length[FindEulerianCycle[CycleGraph[4]]]", "5", 0); + assert_eval_eq("Length[FindEulerianCycle[CompleteGraph[5]]]", "11", 0); + assert_eval_eq("First[FindEulerianCycle[CycleGraph[4]]] === Last[FindEulerianCycle[CycleGraph[4]]]", "True", 0); + /* Directed Eulerian circuit. */ + assert_eval_eq("Length[FindEulerianCycle[Graph[{1,2,3},{1->2,2->3,3->1}]]]", "4", 0); + /* Non-Eulerian (odd degree, disconnected, open directed, edgeless) → {}. */ + assert_eval_eq("FindEulerianCycle[PathGraph[3]]", "{}", 0); + assert_eval_eq("FindEulerianCycle[CompleteGraph[4]]", "{}", 0); + assert_eval_eq("FindEulerianCycle[Graph[{1,2,3,4,5,6},{1<->2,2<->3,3<->1,4<->5,5<->6,6<->4}]]", "{}", 0); + assert_eval_eq("FindEulerianCycle[Graph[{1,2,3},{1->2,2->3}]]", "{}", 0); + assert_eval_eq("FindEulerianCycle[Graph[{1,2},{}]]", "{}", 0); + assert_eval_eq("Head[FindEulerianCycle[5]]", "FindEulerianCycle", 0); +} + +static void test_find_hamiltonian(void) { + /* Cycles/complete graphs have Hamiltonian cycles: n+1 closed vertex list. */ + assert_eval_eq("FindHamiltonianCycle[CycleGraph[3]]", "{1, 2, 3, 1}", 0); + assert_eval_eq("Length[FindHamiltonianCycle[CycleGraph[5]]]", "6", 0); + assert_eval_eq("Length[FindHamiltonianCycle[CompleteGraph[4]]]", "5", 0); + assert_eval_eq("Sort[Union[FindHamiltonianCycle[CompleteGraph[4]]]]", "{1, 2, 3, 4}", 0); + assert_eval_eq("First[FindHamiltonianCycle[CycleGraph[5]]] === Last[FindHamiltonianCycle[CycleGraph[5]]]", "True", 0); + assert_eval_eq("Length[FindHamiltonianCycle[WheelGraph[5]]]", "6", 0); + /* Directed Hamiltonian cycle follows arc direction. */ + assert_eval_eq("Length[FindHamiltonianCycle[Graph[{1,2,3},{1->2,2->3,3->1}]]]", "4", 0); + /* No Hamiltonian cycle: paths, stars, n<3, edgeless, disconnected, broken directed → {}. */ + assert_eval_eq("FindHamiltonianCycle[PathGraph[4]]", "{}", 0); + assert_eval_eq("FindHamiltonianCycle[StarGraph[5]]", "{}", 0); + assert_eval_eq("FindHamiltonianCycle[Graph[{1,2},{1<->2}]]", "{}", 0); + assert_eval_eq("FindHamiltonianCycle[Graph[{1,2,3},{}]]", "{}", 0); + assert_eval_eq("FindHamiltonianCycle[Graph[{1,2,3,4,5,6},{1<->2,2<->3,3<->1,4<->5,5<->6,6<->4}]]", "{}", 0); + assert_eval_eq("FindHamiltonianCycle[Graph[{1,2,3},{1->2,2->3,1->3}]]", "{}", 0); + assert_eval_eq("Head[FindHamiltonianCycle[7]]", "FindHamiltonianCycle", 0); +} + +static void test_graph_power(void) { + /* Undirected: distance-<=k closure. P4^2 adds {1,3},{2,4}; P4^3 = K4. */ + assert_eval_eq("EdgeCount[GraphPower[PathGraph[4], 2]]", "5", 0); + assert_eval_eq("EdgeCount[GraphPower[PathGraph[4], 3]]", "6", 0); + assert_eval_eq("EdgeCount[GraphPower[CycleGraph[5], 2]]", "10", 0); /* K5 */ + assert_eval_eq("EdgeCount[GraphPower[CycleGraph[5], 1]]", "5", 0); /* unchanged */ + assert_eval_eq("VertexList[GraphPower[PathGraph[4], 2]]", "{1, 2, 3, 4}", 0); + assert_eval_eq("FreeQ[EdgeList[GraphPower[CycleGraph[4], 2]], UndirectedEdge[x_, x_]]", "True", 0); + assert_eval_eq("EdgeCount[GraphPower[Graph[{1,2,3},{}], 2]]", "0", 0); + /* Directed: reachability follows arcs; power stays directed. */ + assert_eval_eq("DirectedGraphQ[GraphPower[Graph[{1,2,3},{1->2,2->3}], 2]]", "True", 0); + assert_eval_eq("MemberQ[EdgeList[GraphPower[Graph[{1,2,3},{1->2,2->3}], 2]], DirectedEdge[1,3]]", "True", 0); + /* Non-positive / symbolic k stays unevaluated. */ + assert_eval_eq("Head[GraphPower[CycleGraph[3], 0]]", "GraphPower", 0); + assert_eval_eq("Head[GraphPower[CycleGraph[3], k]]", "GraphPower", 0); +} + +static void test_find_cycle(void) { + /* Undirected: a cycle as {{edges...}}, or {} when acyclic. */ + assert_eval_eq("FindCycle[CycleGraph[3]]", "{{1 <-> 2, 2 <-> 3, 3 <-> 1}}", 0); + assert_eval_eq("Length[First[FindCycle[CycleGraph[4]]]]", "4", 0); + assert_eval_eq("MatchQ[First[FindCycle[CycleGraph[4]]], {UndirectedEdge[_,_]..}]", "True", 0); + assert_eval_eq("FindCycle[CompleteGraph[5]] =!= {}", "True", 0); + assert_eval_eq("FindCycle[PathGraph[5]]", "{}", 0); + assert_eval_eq("FindCycle[Graph[{1,2,3,4},{1<->2,2<->3,3<->4}]]", "{}", 0); + assert_eval_eq("FindCycle[Graph[{1,2,3},{}]]", "{}", 0); + /* The returned edges close up into a cycle. */ + assert_eval_eq("With[{c=First[FindCycle[CompleteGraph[5]]]}, First[First[c]] === Last[Last[c]]]", "True", 0); + /* Directed: cycles follow arcs; DAGs give {}. */ + assert_eval_eq("FindCycle[Graph[{1,2,3},{1->2,2->3,3->1}]]", "{{1 -> 2, 2 -> 3, 3 -> 1}}", 0); + assert_eval_eq("FindCycle[Graph[{1,2},{1->2,2->1}]]", "{{1 -> 2, 2 -> 1}}", 0); + assert_eval_eq("FindCycle[Graph[{1,2,3},{1->2,2->3,1->3}]]", "{}", 0); + assert_eval_eq("Head[FindCycle[9]]", "FindCycle", 0); +} + +static void test_graph_distance_matrix(void) { + /* Undirected: symmetric, zero diagonal, BFS distances. */ + assert_eval_eq("GraphDistanceMatrix[PathGraph[3]]", "{{0, 1, 2}, {1, 0, 1}, {2, 1, 0}}", 0); + assert_eval_eq("GraphDistanceMatrix[CompleteGraph[3]]", "{{0, 1, 1}, {1, 0, 1}, {1, 1, 0}}", 0); + assert_eval_eq("Tr[GraphDistanceMatrix[CompleteGraph[5]]]", "0", 0); + assert_eval_eq("With[{m=GraphDistanceMatrix[CycleGraph[5]]}, m === Transpose[m]]", "True", 0); + assert_eval_eq("GraphDistanceMatrix[CycleGraph[6]][[1,4]]", "3", 0); + /* Unreachable pairs are Infinity. */ + assert_eval_eq("GraphDistanceMatrix[Graph[{1,2,3},{1<->2}]]", "{{0, 1, Infinity}, {1, 0, Infinity}, {Infinity, Infinity, 0}}", 0); + assert_eval_eq("GraphDistanceMatrix[Graph[{1,2},{}]]", "{{0, Infinity}, {Infinity, 0}}", 0); + assert_eval_eq("GraphDistanceMatrix[Graph[{1},{}]]", "{{0}}", 0); + /* Directed: follows arc direction (asymmetric). */ + assert_eval_eq("GraphDistanceMatrix[Graph[{1,2,3},{1->2,2->3}]]", "{{0, 1, 2}, {Infinity, 0, 1}, {Infinity, Infinity, 0}}", 0); + /* Agrees with the pairwise GraphDistance builtin. */ + assert_eval_eq("GraphDistanceMatrix[PathGraph[4]][[1,4]] == GraphDistance[PathGraph[4],1,4]", "True", 0); + assert_eval_eq("Head[GraphDistanceMatrix[5]]", "GraphDistanceMatrix", 0); +} + +static void test_graph_density(void) { + /* Undirected: 2m / (n(n-1)), reduced exact rational. */ + assert_eval_eq("GraphDensity[CompleteGraph[5]]", "1", 0); + assert_eval_eq("GraphDensity[Graph[{1,2,3,4},{}]]", "0", 0); + assert_eval_eq("GraphDensity[CycleGraph[4]]", "2/3", 0); + assert_eval_eq("GraphDensity[PathGraph[4]]", "1/2", 0); + assert_eval_eq("GraphDensity[StarGraph[5]]", "2/5", 0); + assert_eval_eq("GraphDensity[Graph[{1,2},{1<->2}]]", "1", 0); + assert_eval_eq("GraphDensity[Graph[{1},{}]]", "0", 0); + /* Directed: m / (n(n-1)); a complete digraph is 1. */ + assert_eval_eq("GraphDensity[Graph[{1,2,3},{1->2,2->3}]]", "1/3", 0); + assert_eval_eq("GraphDensity[Graph[{1,2,3},{1->2,2->1,1->3,3->1,2->3,3->2}]]", "1", 0); + assert_eval_eq("Head[GraphDensity[7]]", "GraphDensity", 0); +} + +static void test_degree_centrality(void) { + assert_eval_eq("DegreeCentrality[CycleGraph[4]]", "{2, 2, 2, 2}", 0); + assert_eval_eq("DegreeCentrality[CompleteGraph[4]]", "{3, 3, 3, 3}", 0); + assert_eval_eq("DegreeCentrality[StarGraph[5]]", "{4, 1, 1, 1, 1}", 0); + assert_eval_eq("DegreeCentrality[PathGraph[4]]", "{1, 2, 2, 1}", 0); + assert_eval_eq("DegreeCentrality[Graph[{1,2,3},{}]]", "{0, 0, 0}", 0); + /* Directed: in-degree + out-degree. */ + assert_eval_eq("DegreeCentrality[Graph[{1,2,3},{1->2,2->3}]]", "{1, 2, 1}", 0); + /* Consistency: matches VertexDegree (undirected) and obeys the handshake lemma. */ + assert_eval_eq("DegreeCentrality[CycleGraph[5]] == VertexDegree[CycleGraph[5]]", "True", 0); + assert_eval_eq("Total[DegreeCentrality[CompleteGraph[6]]] == 2*EdgeCount[CompleteGraph[6]]", "True", 0); + assert_eval_eq("Head[DegreeCentrality[5]]", "DegreeCentrality", 0); +} + +static void test_find_hamiltonian_path(void) { + assert_eval_eq("FindHamiltonianPath[PathGraph[4]]", "{1, 2, 3, 4}", 0); + assert_eval_eq("Length[FindHamiltonianPath[CycleGraph[4]]]", "4", 0); + assert_eval_eq("Sort[FindHamiltonianPath[CompleteGraph[4]]]", "{1, 2, 3, 4}", 0); + assert_eval_eq("With[{p=FindHamiltonianPath[CompleteGraph[5]]}, Length[Union[p]] == 5]", "True", 0); + assert_eval_eq("FindHamiltonianPath[Graph[{1},{}]]", "{1}", 0); + /* No Hamiltonian path: big star, edgeless, disconnected → {}. */ + assert_eval_eq("FindHamiltonianPath[StarGraph[5]]", "{}", 0); + assert_eval_eq("FindHamiltonianPath[Graph[{1,2,3},{}]]", "{}", 0); + assert_eval_eq("FindHamiltonianPath[Graph[{1,2,3,4},{1<->2,3<->4}]]", "{}", 0); + /* Directed follows arcs. */ + assert_eval_eq("FindHamiltonianPath[Graph[{1,2,3},{1->2,2->3}]]", "{1, 2, 3}", 0); + assert_eval_eq("FindHamiltonianPath[Graph[{1,2,3},{1->2,3->2}]]", "{}", 0); + /* A path graph has a Hamiltonian path but no Hamiltonian cycle. */ + assert_eval_eq("FindHamiltonianPath[PathGraph[4]] =!= {} && FindHamiltonianCycle[PathGraph[4]] === {}", "True", 0); + assert_eval_eq("Head[FindHamiltonianPath[5]]", "FindHamiltonianPath", 0); +} + +static void test_kcore_components(void) { + /* Complete/cycle cores: whole graph at the degree, empty just above. */ + assert_eval_eq("KCoreComponents[CompleteGraph[4], 3]", "{{1, 2, 3, 4}}", 0); + assert_eval_eq("KCoreComponents[CompleteGraph[4], 4]", "{}", 0); + assert_eval_eq("KCoreComponents[CycleGraph[5], 2]", "{{1, 2, 3, 4, 5}}", 0); + assert_eval_eq("KCoreComponents[CycleGraph[5], 3]", "{}", 0); + /* Cascade peeling: a path collapses entirely at k = 2. */ + assert_eval_eq("KCoreComponents[PathGraph[4], 1]", "{{1, 2, 3, 4}}", 0); + assert_eval_eq("KCoreComponents[PathGraph[4], 2]", "{}", 0); + /* k = 0 keeps every vertex; edgeless → singletons, empty at k = 1. */ + assert_eval_eq("KCoreComponents[Graph[{1,2,3},{}], 0]", "{{1}, {2}, {3}}", 0); + assert_eval_eq("KCoreComponents[Graph[{1,2,3},{}], 1]", "{}", 0); + /* Multiple components and pendant removal. */ + assert_eval_eq("KCoreComponents[Graph[{1,2,3,4,5,6},{1<->2,2<->3,3<->1,4<->5,5<->6,6<->4}], 2]", "{{1, 2, 3}, {4, 5, 6}}", 0); + assert_eval_eq("KCoreComponents[Graph[{1,2,3,4},{1<->2,2<->3,3<->1,3<->4}], 2]", "{{1, 2, 3}}", 0); + /* Direction ignored (underlying undirected graph). */ + assert_eval_eq("KCoreComponents[Graph[{1,2,3},{1->2,2->3,3->1}], 2]", "{{1, 2, 3}}", 0); + assert_eval_eq("Head[KCoreComponents[CycleGraph[3], k]]", "KCoreComponents", 0); + assert_eval_eq("Head[KCoreComponents[5, 2]]", "KCoreComponents", 0); +} + +static void test_local_clustering(void) { + /* Cliques → 1 everywhere; triangle-free (cycles, stars, paths) → 0. */ + assert_eval_eq("LocalClusteringCoefficient[CompleteGraph[4]]", "{1, 1, 1, 1}", 0); + assert_eval_eq("LocalClusteringCoefficient[CompleteGraph[3]]", "{1, 1, 1}", 0); + assert_eval_eq("LocalClusteringCoefficient[CycleGraph[5]]", "{0, 0, 0, 0, 0}", 0); + assert_eval_eq("LocalClusteringCoefficient[PathGraph[3]]", "{0, 0, 0}", 0); + assert_eval_eq("LocalClusteringCoefficient[StarGraph[5]]", "{0, 0, 0, 0, 0}", 0); + assert_eval_eq("LocalClusteringCoefficient[Graph[{1,2,3},{}]]", "{0, 0, 0}", 0); + /* Fractional cases: exact rationals. */ + assert_eval_eq("LocalClusteringCoefficient[Graph[{1,2,3,4},{1<->2,2<->3,3<->1,3<->4}]]", "{1, 1, 1/3, 0}", 0); + assert_eval_eq("LocalClusteringCoefficient[Graph[{1,2,3,4},{1<->2,1<->3,1<->4,2<->3,2<->4}]]", "{2/3, 2/3, 1, 1}", 0); + /* Direction ignored (underlying undirected graph). */ + assert_eval_eq("LocalClusteringCoefficient[Graph[{1,2,3},{1->2,2->3,3->1}]]", "{1, 1, 1}", 0); + assert_eval_eq("Head[LocalClusteringCoefficient[5]]", "LocalClusteringCoefficient", 0); +} + +static void test_global_clustering(void) { + /* Cliques → 1; triangle-free → 0. */ + assert_eval_eq("GlobalClusteringCoefficient[CompleteGraph[4]]", "1", 0); + assert_eval_eq("GlobalClusteringCoefficient[CompleteGraph[3]]", "1", 0); + assert_eval_eq("GlobalClusteringCoefficient[CycleGraph[5]]", "0", 0); + assert_eval_eq("GlobalClusteringCoefficient[PathGraph[3]]", "0", 0); + assert_eval_eq("GlobalClusteringCoefficient[StarGraph[6]]", "0", 0); + assert_eval_eq("GlobalClusteringCoefficient[Graph[{1,2},{1<->2}]]", "0", 0); + assert_eval_eq("GlobalClusteringCoefficient[Graph[{1,2,3},{}]]", "0", 0); + /* Fractional transitivity: exact rationals. */ + assert_eval_eq("GlobalClusteringCoefficient[Graph[{1,2,3,4},{1<->2,2<->3,3<->1,3<->4}]]", "3/5", 0); + assert_eval_eq("GlobalClusteringCoefficient[Graph[{1,2,3,4},{1<->2,1<->3,1<->4,2<->3,2<->4}]]", "3/4", 0); + /* Direction ignored. */ + assert_eval_eq("GlobalClusteringCoefficient[Graph[{1,2,3},{1->2,2->3,3->1}]]", "1", 0); + assert_eval_eq("Head[GlobalClusteringCoefficient[5]]", "GlobalClusteringCoefficient", 0); +} + +static void test_mean_clustering(void) { + assert_eval_eq("MeanClusteringCoefficient[CompleteGraph[4]]", "1", 0); + assert_eval_eq("MeanClusteringCoefficient[CycleGraph[5]]", "0", 0); + assert_eval_eq("MeanClusteringCoefficient[StarGraph[6]]", "0", 0); + assert_eval_eq("MeanClusteringCoefficient[Graph[{1,2,3},{}]]", "0", 0); + assert_eval_eq("MeanClusteringCoefficient[Graph[{1,2,3,4},{1<->2,2<->3,3<->1,3<->4}]]", "7/12", 0); + assert_eval_eq("MeanClusteringCoefficient[Graph[{1,2,3,4},{1<->2,1<->3,1<->4,2<->3,2<->4}]]", "5/6", 0); + /* Equals the mean of the local coefficients; differs from global transitivity. */ + assert_eval_eq("MeanClusteringCoefficient[Graph[{1,2,3,4},{1<->2,2<->3,3<->1,3<->4}]] == Total[LocalClusteringCoefficient[Graph[{1,2,3,4},{1<->2,2<->3,3<->1,3<->4}]]]/4", "True", 0); + assert_eval_eq("MeanClusteringCoefficient[Graph[{1,2,3,4},{1<->2,1<->3,1<->4,2<->3,2<->4}]] != GlobalClusteringCoefficient[Graph[{1,2,3,4},{1<->2,1<->3,1<->4,2<->3,2<->4}]]", "True", 0); + assert_eval_eq("MeanClusteringCoefficient[Graph[{1,2,3},{1->2,2->3,3->1}]]", "1", 0); + assert_eval_eq("Head[MeanClusteringCoefficient[5]]", "MeanClusteringCoefficient", 0); +} + +static void test_find_clique(void) { + /* Cliques of complete graphs are the whole graph. */ + assert_eval_eq("FindClique[CompleteGraph[4]]", "{{1, 2, 3, 4}}", 0); + assert_eval_eq("FindClique[CompleteGraph[3]]", "{{1, 2, 3}}", 0); + assert_eval_eq("FindClique[Graph[{1,2,3},{1<->2,2<->3,3<->1}]]", "{{1, 2, 3}}", 0); + assert_eval_eq("FindClique[Graph[{1,2},{1<->2}]]", "{{1, 2}}", 0); + /* Max clique sizes. */ + assert_eval_eq("Length[First[FindClique[CycleGraph[5]]]]", "2", 0); + assert_eval_eq("Length[First[FindClique[CycleGraph[4]]]]", "2", 0); + assert_eval_eq("Length[First[FindClique[Graph[{1,2,3,4},{1<->2,1<->3,1<->4,2<->3,2<->4}]]]]", "3", 0); + assert_eval_eq("Length[First[FindClique[Graph[{1,2,3,4,5,6},{1<->2,2<->3,3<->1,4<->5,5<->6,6<->4}]]]]", "3", 0); + assert_eval_eq("Length[First[FindClique[Graph[{1,2,3},{}]]]]", "1", 0); + /* Direction ignored. */ + assert_eval_eq("Length[First[FindClique[Graph[{1,2,3},{1->2,2->3,3->1}]]]]", "3", 0); + assert_eval_eq("Head[FindClique[5]]", "FindClique", 0); +} + +static void test_find_independent(void) { + /* Edgeless → all vertices; complete → a singleton. */ + assert_eval_eq("FindIndependentVertexSet[Graph[{1,2,3},{}]]", "{{1, 2, 3}}", 0); + assert_eval_eq("Length[First[FindIndependentVertexSet[CompleteGraph[4]]]]", "1", 0); + /* Cycles / paths / star sizes. */ + assert_eval_eq("FindIndependentVertexSet[CycleGraph[4]]", "{{1, 3}}", 0); + assert_eval_eq("Length[First[FindIndependentVertexSet[CycleGraph[5]]]]", "2", 0); + assert_eval_eq("FindIndependentVertexSet[StarGraph[5]]", "{{2, 3, 4, 5}}", 0); + assert_eval_eq("Length[First[FindIndependentVertexSet[PathGraph[4]]]]", "2", 0); + /* Duality: max independent set of g = max clique of the complement. */ + assert_eval_eq("Length[First[FindIndependentVertexSet[CycleGraph[5]]]] == Length[First[FindClique[GraphComplement[CycleGraph[5]]]]]", "True", 0); + /* Direction ignored. */ + assert_eval_eq("Length[First[FindIndependentVertexSet[Graph[{1,2,3},{1->2,2->3}]]]]", "2", 0); + assert_eval_eq("Head[FindIndependentVertexSet[5]]", "FindIndependentVertexSet", 0); +} + +static void test_find_vertex_cover(void) { + /* Edgeless → empty cover; complete K_n → n-1. */ + assert_eval_eq("FindVertexCover[Graph[{1,2,3},{}]]", "{}", 0); + assert_eval_eq("Length[FindVertexCover[CompleteGraph[4]]]", "3", 0); + /* Cycle / path / star / single edge sizes. */ + assert_eval_eq("FindVertexCover[CycleGraph[4]]", "{2, 4}", 0); + assert_eval_eq("Length[FindVertexCover[PathGraph[4]]]", "2", 0); + assert_eval_eq("FindVertexCover[StarGraph[5]]", "{1}", 0); + assert_eval_eq("Length[FindVertexCover[Graph[{1,2},{1<->2}]]]", "1", 0); + assert_eval_eq("Length[FindVertexCover[Graph[{1,2,3},{1<->2,2<->3,3<->1}]]]", "2", 0); + /* Gallai identity: |min cover| + |max independent set| = n. */ + assert_eval_eq("Length[FindVertexCover[CycleGraph[5]]] + Length[First[FindIndependentVertexSet[CycleGraph[5]]]] == 5", "True", 0); + assert_eval_eq("Length[FindVertexCover[CompleteGraph[5]]] + Length[First[FindIndependentVertexSet[CompleteGraph[5]]]] == 5", "True", 0); + /* Direction ignored. */ + assert_eval_eq("Length[FindVertexCover[Graph[{1,2,3},{1->2,2->3}]]]", "1", 0); + assert_eval_eq("Head[FindVertexCover[5]]", "FindVertexCover", 0); +} + +static void test_graph_reciprocity(void) { + /* Undirected graphs are fully reciprocal. */ + assert_eval_eq("GraphReciprocity[CycleGraph[4]]", "1", 0); + assert_eval_eq("GraphReciprocity[CompleteGraph[5]]", "1", 0); + assert_eval_eq("GraphReciprocity[Graph[{1,2},{1<->2}]]", "1", 0); + /* Directed reciprocity is the fraction of reciprocated arcs. */ + assert_eval_eq("GraphReciprocity[Graph[{1,2,3},{1->2,2->3,3->1}]]", "0", 0); + assert_eval_eq("GraphReciprocity[Graph[{1,2},{1->2,2->1}]]", "1", 0); + assert_eval_eq("GraphReciprocity[Graph[{1,2,3},{1->2,2->1,2->3}]]", "2/3", 0); + assert_eval_eq("GraphReciprocity[Graph[{1,2,3,4},{1->2,2->1,3->4}]]", "2/3", 0); + assert_eval_eq("GraphReciprocity[Graph[{1,2},{1->2}]]", "0", 0); + /* No edges → 0 by convention. */ + assert_eval_eq("GraphReciprocity[Graph[{1,2,3},{}]]", "0", 0); + assert_eval_eq("Head[GraphReciprocity[5]]", "GraphReciprocity", 0); +} + +static void test_chromatic_polynomial(void) { + /* Numeric k gives the proper-colouring count. */ + assert_eval_eq("ChromaticPolynomial[Graph[{1,2,3},{}], 2]", "8", 0); /* k^3 */ + assert_eval_eq("ChromaticPolynomial[CompleteGraph[3], 3]", "6", 0); /* 3! */ + assert_eval_eq("ChromaticPolynomial[CompleteGraph[3], 2]", "0", 0); /* not 2-colorable */ + assert_eval_eq("ChromaticPolynomial[CompleteGraph[4], 4]", "24", 0); + assert_eval_eq("ChromaticPolynomial[PathGraph[3], 2]", "2", 0); + assert_eval_eq("ChromaticPolynomial[CycleGraph[4], 3]", "18", 0); + assert_eval_eq("ChromaticPolynomial[CycleGraph[5], 3]", "30", 0); + assert_eval_eq("ChromaticPolynomial[Graph[{1},{}], 7]", "7", 0); + /* Bipartite → 2-colorable; odd cycle → not. */ + assert_eval_eq("ChromaticPolynomial[CycleGraph[4], 2]", "2", 0); + assert_eval_eq("ChromaticPolynomial[CycleGraph[5], 2]", "0", 0); + /* Symbolic k yields a polynomial; substitute to check. */ + assert_eval_eq("ChromaticPolynomial[Graph[{1,2},{1<->2}], k] /. k->5", "20", 0); + assert_eval_eq("ChromaticPolynomial[CompleteGraph[3], k] /. k->4", "24", 0); + /* Oversized / non-graph stays unevaluated. */ + assert_eval_eq("Head[ChromaticPolynomial[CompleteGraph[9], k]]", "ChromaticPolynomial", 0); + assert_eval_eq("Head[ChromaticPolynomial[5, k]]", "ChromaticPolynomial", 0); +} + +static void test_chromatic_number(void) { + assert_eval_eq("ChromaticNumber[Graph[{1,2,3},{}]]", "1", 0); + assert_eval_eq("ChromaticNumber[Graph[{1},{}]]", "1", 0); + assert_eval_eq("ChromaticNumber[CompleteGraph[4]]", "4", 0); + assert_eval_eq("ChromaticNumber[CompleteGraph[5]]", "5", 0); + assert_eval_eq("ChromaticNumber[CycleGraph[4]]", "2", 0); /* bipartite */ + assert_eval_eq("ChromaticNumber[CycleGraph[5]]", "3", 0); /* odd cycle */ + assert_eval_eq("ChromaticNumber[Graph[{1,2,3},{1<->2,2<->3,3<->1}]]", "3", 0); + assert_eval_eq("ChromaticNumber[PathGraph[4]]", "2", 0); + assert_eval_eq("ChromaticNumber[StarGraph[6]]", "2", 0); + assert_eval_eq("ChromaticNumber[WheelGraph[6]]", "4", 0); /* odd rim */ + assert_eval_eq("ChromaticNumber[WheelGraph[7]]", "3", 0); /* even rim */ + /* Consistent with the chromatic polynomial: not 2-colorable ⇒ P(2)=0. */ + assert_eval_eq("ChromaticNumber[CycleGraph[5]] == 3 && ChromaticPolynomial[CycleGraph[5],2]==0", "True", 0); + assert_eval_eq("ChromaticNumber[Graph[{1,2,3},{1->2,2->3,3->1}]]", "3", 0); + assert_eval_eq("Head[ChromaticNumber[5]]", "ChromaticNumber", 0); +} + +static void test_degree_sequence(void) { + assert_eval_eq("DegreeSequence[CompleteGraph[4]]", "{3, 3, 3, 3}", 0); + assert_eval_eq("DegreeSequence[StarGraph[5]]", "{4, 1, 1, 1, 1}", 0); + assert_eval_eq("DegreeSequence[PathGraph[4]]", "{2, 2, 1, 1}", 0); + assert_eval_eq("DegreeSequence[CycleGraph[5]]", "{2, 2, 2, 2, 2}", 0); + assert_eval_eq("DegreeSequence[Graph[{1,2,3},{}]]", "{0, 0, 0}", 0); + assert_eval_eq("DegreeSequence[Graph[{1,2,3,4},{1<->2,1<->3,1<->4,2<->3,2<->4}]]", "{3, 3, 2, 2}", 0); + /* Directed: total degree, sorted descending. */ + assert_eval_eq("DegreeSequence[Graph[{1,2,3},{1->2,2->3}]]", "{2, 1, 1}", 0); + /* Handshake lemma and permutation-of-DegreeCentrality consistency. */ + assert_eval_eq("Total[DegreeSequence[CompleteGraph[6]]] == 2*EdgeCount[CompleteGraph[6]]", "True", 0); + assert_eval_eq("Sort[DegreeSequence[StarGraph[5]]] == Sort[DegreeCentrality[StarGraph[5]]]", "True", 0); + assert_eval_eq("Head[DegreeSequence[5]]", "DegreeSequence", 0); +} + +static void test_tree_graph_q(void) { + assert_eval_eq("TreeGraphQ[PathGraph[4]]", "True", 0); + assert_eval_eq("TreeGraphQ[StarGraph[5]]", "True", 0); + assert_eval_eq("TreeGraphQ[Graph[{1},{}]]", "True", 0); + assert_eval_eq("TreeGraphQ[Graph[{1,2,3,4,5},{1<->2,1<->3,2<->4,2<->5}]]", "True", 0); + assert_eval_eq("TreeGraphQ[Graph[{1,2,3},{1->2,2->3}]]", "True", 0); + /* Not trees: cycles, complete, edgeless multi-vertex, forests. */ + assert_eval_eq("TreeGraphQ[CycleGraph[4]]", "False", 0); + assert_eval_eq("TreeGraphQ[CompleteGraph[4]]", "False", 0); + assert_eval_eq("TreeGraphQ[Graph[{1,2,3},{}]]", "False", 0); + assert_eval_eq("TreeGraphQ[Graph[{1,2,3,4},{1<->2,3<->4}]]", "False", 0); + assert_eval_eq("TreeGraphQ[Graph[{1,2,3},{1<->2,2<->3,3<->1}]]", "False", 0); + /* Characterisation: tree ⇔ connected and n-1 edges. */ + assert_eval_eq("TreeGraphQ[PathGraph[5]] == (ConnectedGraphQ[PathGraph[5]] && EdgeCount[PathGraph[5]]==4)", "True", 0); + assert_eval_eq("Head[TreeGraphQ[5]]", "TreeGraphQ", 0); +} + +static void test_strongly_connected_q(void) { + /* Directed: needs cycles reaching both ways. */ + assert_eval_eq("StronglyConnectedGraphQ[Graph[{1,2,3},{1->2,2->3,3->1}]]", "True", 0); + assert_eval_eq("StronglyConnectedGraphQ[Graph[{1,2,3},{1->2,2->3}]]", "False", 0); + assert_eval_eq("StronglyConnectedGraphQ[Graph[{1,2},{1->2,2->1}]]", "True", 0); + assert_eval_eq("StronglyConnectedGraphQ[Graph[{1,2,3},{1->2,2->1,2->3}]]", "False", 0); + /* Undirected: coincides with connectivity. */ + assert_eval_eq("StronglyConnectedGraphQ[CycleGraph[4]]", "True", 0); + assert_eval_eq("StronglyConnectedGraphQ[PathGraph[4]]", "True", 0); + assert_eval_eq("StronglyConnectedGraphQ[Graph[{1,2,3},{1<->2}]]", "False", 0); + assert_eval_eq("StronglyConnectedGraphQ[Graph[{1},{}]]", "True", 0); + assert_eval_eq("StronglyConnectedGraphQ[Graph[{1,2},{}]]", "False", 0); + /* Consistent with a single all-covering strongly connected component. */ + assert_eval_eq("StronglyConnectedGraphQ[Graph[{1,2,3},{1->2,2->3,3->1}]] == (Length[StronglyConnectedComponents[Graph[{1,2,3},{1->2,2->3,3->1}]]] == 1)", "True", 0); + assert_eval_eq("Head[StronglyConnectedGraphQ[5]]", "StronglyConnectedGraphQ", 0); +} + +static void test_hamiltonian_graph_q(void) { + assert_eval_eq("HamiltonianGraphQ[CycleGraph[5]]", "True", 0); + assert_eval_eq("HamiltonianGraphQ[CompleteGraph[4]]", "True", 0); + assert_eval_eq("HamiltonianGraphQ[WheelGraph[5]]", "True", 0); + assert_eval_eq("HamiltonianGraphQ[Graph[{1,2,3},{1->2,2->3,3->1}]]", "True", 0); + /* Not Hamiltonian: paths, stars, broken directed, edgeless, n<3. */ + assert_eval_eq("HamiltonianGraphQ[PathGraph[4]]", "False", 0); + assert_eval_eq("HamiltonianGraphQ[StarGraph[5]]", "False", 0); + assert_eval_eq("HamiltonianGraphQ[Graph[{1,2,3},{1->2,2->3,1->3}]]", "False", 0); + assert_eval_eq("HamiltonianGraphQ[Graph[{1,2,3},{}]]", "False", 0); + assert_eval_eq("HamiltonianGraphQ[Graph[{1,2},{1<->2}]]", "False", 0); + /* Agrees with FindHamiltonianCycle. */ + assert_eval_eq("HamiltonianGraphQ[CycleGraph[5]] == (FindHamiltonianCycle[CycleGraph[5]] =!= {})", "True", 0); + assert_eval_eq("HamiltonianGraphQ[PathGraph[4]] == (FindHamiltonianCycle[PathGraph[4]] =!= {})", "True", 0); + assert_eval_eq("Head[HamiltonianGraphQ[5]]", "HamiltonianGraphQ", 0); +} + +static void test_regular_graph_q(void) { + assert_eval_eq("RegularGraphQ[CycleGraph[5]]", "True", 0); + assert_eval_eq("RegularGraphQ[CompleteGraph[4]]", "True", 0); + assert_eval_eq("RegularGraphQ[Graph[{1,2,3},{}]]", "True", 0); /* 0-regular */ + assert_eval_eq("RegularGraphQ[Graph[{1},{}]]", "True", 0); + assert_eval_eq("RegularGraphQ[Graph[{1,2,3,4,5,6},{1<->4,1<->5,1<->6,2<->4,2<->5,2<->6,3<->4,3<->5,3<->6}]]", "True", 0); + /* Not regular. */ + assert_eval_eq("RegularGraphQ[PathGraph[4]]", "False", 0); + assert_eval_eq("RegularGraphQ[StarGraph[5]]", "False", 0); + assert_eval_eq("RegularGraphQ[WheelGraph[5]]", "False", 0); + /* Directed: equal in- and out-degrees. */ + assert_eval_eq("RegularGraphQ[Graph[{1,2,3},{1->2,2->3,3->1}]]", "True", 0); + assert_eval_eq("RegularGraphQ[Graph[{1,2,3},{1->2,2->3}]]", "False", 0); + assert_eval_eq("Head[RegularGraphQ[5]]", "RegularGraphQ", 0); +} + +static void test_complete_graph_q(void) { + assert_eval_eq("CompleteGraphQ[CompleteGraph[4]]", "True", 0); + assert_eval_eq("CompleteGraphQ[CompleteGraph[5]]", "True", 0); + assert_eval_eq("CompleteGraphQ[CycleGraph[3]]", "True", 0); /* C3 = K3 */ + assert_eval_eq("CompleteGraphQ[Graph[{1,2},{1<->2}]]", "True", 0); + assert_eval_eq("CompleteGraphQ[Graph[{1},{}]]", "True", 0); + assert_eval_eq("CompleteGraphQ[Graph[{1,2,3},{1->2,2->3,3->1}]]", "True", 0); + /* Not complete. */ + assert_eval_eq("CompleteGraphQ[CycleGraph[4]]", "False", 0); + assert_eval_eq("CompleteGraphQ[PathGraph[4]]", "False", 0); + assert_eval_eq("CompleteGraphQ[Graph[{1,2,3},{}]]", "False", 0); + assert_eval_eq("CompleteGraphQ[Graph[{1,2,3,4},{1<->2,1<->3,1<->4,2<->3,2<->4}]]", "False", 0); + assert_eval_eq("Head[CompleteGraphQ[5]]", "CompleteGraphQ", 0); +} + +static void test_graph_union(void) { + /* Union merges vertices and edges. */ + assert_eval_eq("VertexList[GraphUnion[Graph[{1,2},{1<->2}], Graph[{2,3},{2<->3}]]]", "{1, 2, 3}", 0); + assert_eval_eq("VertexCount[GraphUnion[Graph[{1,2,3},{1<->2,2<->3}], Graph[{3,4,5},{3<->4,4<->5}]]]", "5", 0); + assert_eval_eq("EdgeCount[GraphUnion[Graph[{1,2,3},{1<->2,2<->3}], Graph[{3,4,5},{3<->4,4<->5}]]]", "4", 0); + /* Shared / symmetric edges deduped; self-union is idempotent. */ + assert_eval_eq("EdgeCount[GraphUnion[Graph[{1,2,3},{1<->2,2<->3}], Graph[{2,3,4},{2<->3,3<->4}]]]", "3", 0); + assert_eval_eq("EdgeCount[GraphUnion[Graph[{1,2},{1<->2}], Graph[{1,2},{2<->1}]]]", "1", 0); + assert_eval_eq("EdgeCount[GraphUnion[CycleGraph[4], CycleGraph[4]]] == EdgeCount[CycleGraph[4]]", "True", 0); + /* Directed edges are not symmetric. */ + assert_eval_eq("EdgeCount[GraphUnion[Graph[{1,2},{1->2}], Graph[{1,2},{2->1}]]]", "2", 0); + /* Edgeless union and validity. */ + assert_eval_eq("VertexCount[GraphUnion[Graph[{1,2},{}], Graph[{3,4},{}]]]", "4", 0); + assert_eval_eq("GraphQ[GraphUnion[PathGraph[3], CycleGraph[3]]]", "True", 0); + assert_eval_eq("Head[GraphUnion[5, CycleGraph[3]]]", "GraphUnion", 0); +} + +static void test_graph_intersection(void) { + /* Common vertices and common edges. */ + assert_eval_eq("VertexList[GraphIntersection[Graph[{1,2,3},{1<->2,2<->3}], Graph[{2,3,4},{2<->3,3<->4}]]]", "{2, 3}", 0); + assert_eval_eq("EdgeList[GraphIntersection[Graph[{1,2,3},{1<->2,2<->3}], Graph[{2,3,4},{2<->3,3<->4}]]]", "{2 <-> 3}", 0); + /* Identical graphs → self. */ + assert_eval_eq("EdgeCount[GraphIntersection[CycleGraph[4], CycleGraph[4]]]", "4", 0); + assert_eval_eq("VertexCount[GraphIntersection[CycleGraph[4], CycleGraph[4]]]", "4", 0); + /* Disjoint / no shared edges. */ + assert_eval_eq("VertexCount[GraphIntersection[Graph[{1,2},{1<->2}], Graph[{3,4},{3<->4}]]]", "0", 0); + assert_eval_eq("EdgeCount[GraphIntersection[Graph[{1,2,3},{1<->2}], Graph[{1,2,3},{2<->3}]]]", "0", 0); + /* Symmetric undirected common; directed not symmetric. */ + assert_eval_eq("EdgeCount[GraphIntersection[Graph[{1,2},{1<->2}], Graph[{1,2},{2<->1}]]]", "1", 0); + assert_eval_eq("EdgeCount[GraphIntersection[Graph[{1,2},{1->2}], Graph[{1,2},{2->1}]]]", "0", 0); + assert_eval_eq("EdgeCount[GraphIntersection[CompleteGraph[4], CycleGraph[4]]]", "4", 0); + assert_eval_eq("Head[GraphIntersection[5, CycleGraph[3]]]", "GraphIntersection", 0); +} + +static void test_graph_difference(void) { + /* K4 minus C4 leaves the two diagonals, keeps all vertices. */ + assert_eval_eq("EdgeCount[GraphDifference[CompleteGraph[4], CycleGraph[4]]]", "2", 0); + assert_eval_eq("VertexCount[GraphDifference[CompleteGraph[4], CycleGraph[4]]]", "4", 0); + /* Self-difference: edgeless on the same vertices. */ + assert_eval_eq("EdgeCount[GraphDifference[CycleGraph[5], CycleGraph[5]]]", "0", 0); + assert_eval_eq("VertexCount[GraphDifference[CycleGraph[5], CycleGraph[5]]]", "5", 0); + /* Disjoint g2 removes nothing; a shared edge is removed. */ + assert_eval_eq("EdgeCount[GraphDifference[Graph[{1,2,3},{1<->2,2<->3}], Graph[{4,5},{4<->5}]]]", "2", 0); + assert_eval_eq("EdgeList[GraphDifference[Graph[{1,2,3},{1<->2,2<->3}], Graph[{2,3},{2<->3}]]]", "{1 <-> 2}", 0); + /* Symmetric undirected removal; directed not symmetric. */ + assert_eval_eq("EdgeCount[GraphDifference[Graph[{1,2},{1<->2}], Graph[{1,2},{2<->1}]]]", "0", 0); + assert_eval_eq("EdgeCount[GraphDifference[Graph[{1,2},{1->2}], Graph[{1,2},{2->1}]]]", "1", 0); + assert_eval_eq("GraphQ[GraphDifference[CompleteGraph[5], CycleGraph[5]]]", "True", 0); + assert_eval_eq("Head[GraphDifference[5, CycleGraph[3]]]", "GraphDifference", 0); +} + +static void test_graph_reverse(void) { + assert_eval_eq("EdgeList[ReverseGraph[Graph[{1,2,3},{1->2,2->3}]]]", "{2 -> 1, 3 -> 2}", 0); + assert_eval_eq("VertexList[ReverseGraph[Graph[{1,2,3},{1->2,2->3}]]]", "{1, 2, 3}", 0); + assert_eval_eq("EdgeCount[ReverseGraph[Graph[{1,2,3},{1->2,2->3,3->1}]]]", "3", 0); + /* Undirected edges are unchanged; reversal is an involution on directed graphs. */ + assert_eval_eq("EdgeList[ReverseGraph[Graph[{1,2},{1<->2}]]]", "{1 <-> 2}", 0); + assert_eval_eq("ReverseGraph[ReverseGraph[Graph[{1,2,3},{1->2,2->3}]]] === Graph[{1,2,3},{1->2,2->3}]", "True", 0); + /* Swaps in- and out-degree. */ + assert_eval_eq("VertexOutDegree[ReverseGraph[Graph[{1,2,3},{1->2,1->3}]], 1]", "0", 0); + assert_eval_eq("VertexInDegree[ReverseGraph[Graph[{1,2,3},{1->2,1->3}]], 1]", "2", 0); + /* A reversed directed cycle is still strongly connected. */ + assert_eval_eq("StronglyConnectedGraphQ[ReverseGraph[Graph[{1,2,3},{1->2,2->3,3->1}]]]", "True", 0); + assert_eval_eq("GraphQ[ReverseGraph[Graph[{1,2},{1->2}]]]", "True", 0); + assert_eval_eq("Head[ReverseGraph[5]]", "ReverseGraph", 0); +} + +static void test_path_graph_q(void) { + assert_eval_eq("PathGraphQ[PathGraph[4]]", "True", 0); + assert_eval_eq("PathGraphQ[Graph[{1},{}]]", "True", 0); + assert_eval_eq("PathGraphQ[Graph[{1,2},{1<->2}]]", "True", 0); + assert_eval_eq("PathGraphQ[Graph[{1,2,3},{1->2,2->3}]]", "True", 0); + /* Not paths: cycles, stars, branches, disconnected, edgeless. */ + assert_eval_eq("PathGraphQ[CycleGraph[4]]", "False", 0); + assert_eval_eq("PathGraphQ[StarGraph[4]]", "False", 0); + assert_eval_eq("PathGraphQ[CompleteGraph[3]]", "False", 0); + assert_eval_eq("PathGraphQ[Graph[{1,2,3,4},{1<->2,2<->3,2<->4}]]", "False", 0); + assert_eval_eq("PathGraphQ[Graph[{1,2,3,4},{1<->2,3<->4}]]", "False", 0); + assert_eval_eq("PathGraphQ[Graph[{1,2,3},{}]]", "False", 0); + /* A path is a tree. */ + assert_eval_eq("PathGraphQ[PathGraph[5]] && TreeGraphQ[PathGraph[5]]", "True", 0); + assert_eval_eq("Head[PathGraphQ[5]]", "PathGraphQ", 0); +} + +static void test_vertex_contract(void) { + /* Contracting an edge of a triangle yields a single edge. */ + assert_eval_eq("EdgeList[VertexContract[Graph[{1,2,3},{1<->2,2<->3,3<->1}], {1,2}]]", "{1 <-> 3}", 0); + assert_eval_eq("VertexList[VertexContract[Graph[{1,2,3},{1<->2,2<->3,3<->1}], {1,2}]]", "{1, 3}", 0); + /* Singleton contraction is a no-op. */ + assert_eval_eq("EdgeCount[VertexContract[CycleGraph[4], {1}]]", "4", 0); + assert_eval_eq("VertexCount[VertexContract[CycleGraph[4], {1}]]", "4", 0); + /* Contracting a path's endpoints closes it into a triangle. */ + assert_eval_eq("EdgeCount[VertexContract[PathGraph[4], {1,4}]]", "3", 0); + assert_eval_eq("VertexCount[VertexContract[PathGraph[4], {1,4}]]", "3", 0); + /* Contract all → one vertex, no edges. */ + assert_eval_eq("VertexCount[VertexContract[CompleteGraph[4], {1,2,3,4}]]", "1", 0); + assert_eval_eq("EdgeCount[VertexContract[CompleteGraph[4], {1,2,3,4}]]", "0", 0); + /* Parallel edges collapse; directed self-loops drop. */ + assert_eval_eq("EdgeCount[VertexContract[Graph[{1,2,3},{1<->3,2<->3}], {1,2}]]", "1", 0); + assert_eval_eq("EdgeCount[VertexContract[Graph[{1,2,3},{1->2,2->3}], {1,2}]]", "1", 0); + assert_eval_eq("GraphQ[VertexContract[CompleteGraph[4], {1,2}]]", "True", 0); + assert_eval_eq("Head[VertexContract[CycleGraph[3], {9}]]", "VertexContract", 0); + assert_eval_eq("Head[VertexContract[5, {1}]]", "VertexContract", 0); +} + +static void test_pagerank_centrality(void) { + /* Exact rational vector summing to 1. */ + assert_eval_eq("Total[PageRankCentrality[CycleGraph[4]]]", "1", 0); + assert_eval_eq("Total[PageRankCentrality[StarGraph[5]]]", "1", 0); + assert_eval_eq("Total[PageRankCentrality[Graph[{1,2,3},{1->2,2->3,3->1}]]]", "1", 0); + /* Regular graphs / all-dangling → uniform 1/n. */ + assert_eval_eq("PageRankCentrality[CycleGraph[4]]", "{1/4, 1/4, 1/4, 1/4}", 0); + assert_eval_eq("PageRankCentrality[CompleteGraph[5]]", "{1/5, 1/5, 1/5, 1/5, 1/5}", 0); + assert_eval_eq("PageRankCentrality[Graph[{1,2,3},{}]]", "{1/3, 1/3, 1/3}", 0); + assert_eval_eq("PageRankCentrality[Graph[{1},{}]]", "{1}", 0); + /* Star: exact rationals, centre outranks the leaves. */ + assert_eval_eq("PageRankCentrality[StarGraph[4]]", "{71/148, 77/444, 77/444, 77/444}", 0); + assert_eval_eq("First[PageRankCentrality[StarGraph[5]]] > PageRankCentrality[StarGraph[5]][[2]]", "True", 0); + /* Directed hub. */ + assert_eval_eq("PageRankCentrality[Graph[{1,2,3},{1->2,1->3}]]", "{20/77, 57/154, 57/154}", 0); + assert_eval_eq("Head[PageRankCentrality[5]]", "PageRankCentrality", 0); +} + +static void test_katz_centrality(void) { + /* alpha = 0 → base weights only. */ + assert_eval_eq("KatzCentrality[CycleGraph[4], 0]", "{1, 1, 1, 1}", 0); + assert_eval_eq("KatzCentrality[Graph[{1,2,3},{}], 1/5]", "{1, 1, 1}", 0); + assert_eval_eq("KatzCentrality[Graph[{1},{}], 1/2]", "{1}", 0); + /* Exact rationals; regular graph is uniform, path centre outranks ends. */ + assert_eval_eq("KatzCentrality[Graph[{1,2},{1<->2}], 1/10]", "{10/9, 10/9}", 0); + assert_eval_eq("KatzCentrality[CycleGraph[4], 1/10]", "{5/4, 5/4, 5/4, 5/4}", 0); + assert_eval_eq("KatzCentrality[PathGraph[3], 1/10]", "{55/49, 60/49, 55/49}", 0); + /* Directed: score comes from in-edges (who points at you). */ + assert_eval_eq("KatzCentrality[Graph[{1,2,3},{1->2,1->3}], 1/10][[2]] > KatzCentrality[Graph[{1,2,3},{1->2,1->3}], 1/10][[1]]", "True", 0); + /* Non-numeric alpha / non-graph / arity stay unevaluated. */ + assert_eval_eq("Head[KatzCentrality[CycleGraph[3], a]]", "KatzCentrality", 0); + assert_eval_eq("Head[KatzCentrality[5, 1/10]]", "KatzCentrality", 0); + assert_eval_eq("Head[KatzCentrality[CycleGraph[3]]]", "KatzCentrality", 0); +} + +static void test_graph_join(void) { + /* K1 join K1 = a single edge. */ + assert_eval_eq("EdgeCount[GraphJoin[Graph[{1},{}], Graph[{1},{}]]]", "1", 0); + assert_eval_eq("VertexCount[GraphJoin[Graph[{1},{}], Graph[{1},{}]]]", "2", 0); + /* Edge count = m1 + m2 + n1*n2; vertices relabelled 1..n1+n2. */ + assert_eval_eq("EdgeCount[GraphJoin[PathGraph[3], PathGraph[2]]]", "9", 0); + assert_eval_eq("VertexCount[GraphJoin[PathGraph[3], PathGraph[2]]]", "5", 0); + assert_eval_eq("VertexList[GraphJoin[Graph[{a,b},{a<->b}], Graph[{x},{}]]]", "{1, 2, 3}", 0); + /* P2 join K1 is a triangle; Km join Kn is K(m+n). */ + assert_eval_eq("CompleteGraphQ[GraphJoin[PathGraph[2], Graph[{1},{}]]]", "True", 0); + assert_eval_eq("CompleteGraphQ[GraphJoin[CompleteGraph[2], CompleteGraph[2]]]", "True", 0); + assert_eval_eq("CompleteGraphQ[GraphJoin[CompleteGraph[2], CompleteGraph[3]]]", "True", 0); + assert_eval_eq("GraphQ[GraphJoin[CycleGraph[3], CycleGraph[3]]]", "True", 0); + assert_eval_eq("Head[GraphJoin[5, CycleGraph[3]]]", "GraphJoin", 0); +} + +static void test_index_graph(void) { + /* Symbolic labels → 1..n, edges remapped, kinds preserved. */ + assert_eval_eq("VertexList[IndexGraph[Graph[{x,y,z},{x<->z}]]]", "{1, 2, 3}", 0); + assert_eval_eq("EdgeList[IndexGraph[Graph[{a,b,c},{a<->b,b<->c}]]]", "{1 <-> 2, 2 <-> 3}", 0); + assert_eval_eq("EdgeList[IndexGraph[Graph[{a,b,c},{a->b,b->c}]]]", "{1 -> 2, 2 -> 3}", 0); + assert_eval_eq("VertexList[IndexGraph[Graph[{a,b},{a<->b}], 0]]", "{0, 1}", 0); + assert_eval_eq("EdgeCount[IndexGraph[CompleteGraph[4]]]", "6", 0); + /* Already 1..n is unchanged; structure (edge count) is preserved. */ + assert_eval_eq("IndexGraph[PathGraph[3]] === PathGraph[3]", "True", 0); + assert_eval_eq("EdgeCount[IndexGraph[Graph[{p,q,r},{p<->q,q<->r,r<->p}]]]", "3", 0); + assert_eval_eq("GraphQ[IndexGraph[Graph[{a,b,c},{a<->b}]]]", "True", 0); + /* Non-integer start / non-graph stay unevaluated. */ + assert_eval_eq("Head[IndexGraph[CycleGraph[3], x]]", "IndexGraph", 0); + assert_eval_eq("Head[IndexGraph[5]]", "IndexGraph", 0); +} + +static void test_empty_and_mixed_q(void) { + /* EmptyGraphQ: no edges. */ + assert_eval_eq("EmptyGraphQ[Graph[{1,2,3},{}]]", "True", 0); + assert_eval_eq("EmptyGraphQ[Graph[{1},{}]]", "True", 0); + assert_eval_eq("EmptyGraphQ[CycleGraph[3]]", "False", 0); + assert_eval_eq("EmptyGraphQ[Graph[{1,2},{1<->2}]]", "False", 0); + assert_eval_eq("Head[EmptyGraphQ[5]]", "EmptyGraphQ", 0); + /* MixedGraphQ: both directed and undirected edges present. */ + assert_eval_eq("MixedGraphQ[Graph[{1,2,3},{1->2,2<->3}]]", "True", 0); + assert_eval_eq("MixedGraphQ[Graph[{1,2,3},{1->2,2->3}]]", "False", 0); + assert_eval_eq("MixedGraphQ[CycleGraph[4]]", "False", 0); + assert_eval_eq("MixedGraphQ[Graph[{1,2},{}]]", "False", 0); + assert_eval_eq("Head[MixedGraphQ[5]]", "MixedGraphQ", 0); +} + +static void test_graph_product(void) { + /* P2 [] P2 = C4. */ + assert_eval_eq("VertexCount[GraphProduct[Graph[{1,2},{1<->2}], Graph[{1,2},{1<->2}], \"Cartesian\"]]", "4", 0); + assert_eval_eq("EdgeCount[GraphProduct[Graph[{1,2},{1<->2}], Graph[{1,2},{1<->2}], \"Cartesian\"]]", "4", 0); + /* Tensor / Strong / Lexicographic of K2 with K2. */ + assert_eval_eq("EdgeCount[GraphProduct[Graph[{1,2},{1<->2}], Graph[{1,2},{1<->2}], \"Tensor\"]]", "2", 0); + assert_eval_eq("CompleteGraphQ[GraphProduct[Graph[{1,2},{1<->2}], Graph[{1,2},{1<->2}], \"Strong\"]]", "True", 0); + assert_eval_eq("EdgeCount[GraphProduct[Graph[{1,2},{1<->2}], Graph[{1,2},{1<->2}], \"Lexicographic\"]]", "6", 0); + /* C4 [] K2 is the 3-regular cube (8 vertices, 12 edges). */ + assert_eval_eq("VertexCount[GraphProduct[CycleGraph[4], Graph[{1,2},{1<->2}], \"Cartesian\"]]", "8", 0); + assert_eval_eq("EdgeCount[GraphProduct[CycleGraph[4], Graph[{1,2},{1<->2}], \"Cartesian\"]]", "12", 0); + assert_eval_eq("RegularGraphQ[GraphProduct[CycleGraph[4], Graph[{1,2},{1<->2}], \"Cartesian\"]]", "True", 0); + /* Cartesian grid P3 x P2 has 3*1 + 2*2 = 7 edges. */ + assert_eval_eq("EdgeCount[GraphProduct[PathGraph[3], Graph[{1,2},{1<->2}], \"Cartesian\"]]", "7", 0); + /* Unknown type / non-string / non-graph stay unevaluated. */ + assert_eval_eq("Head[GraphProduct[Graph[{1,2},{1<->2}], Graph[{1,2},{1<->2}], \"Nope\"]]", "GraphProduct", 0); + assert_eval_eq("Head[GraphProduct[5, Graph[{1,2},{1<->2}], \"Cartesian\"]]", "GraphProduct", 0); +} + +static void test_turan_graph(void) { + assert_eval_eq("EdgeCount[TuranGraph[4,2]]", "4", 0); /* C4 */ + assert_eval_eq("EdgeCount[TuranGraph[5,2]]", "6", 0); /* K_{2,3} */ + assert_eval_eq("EdgeCount[TuranGraph[6,3]]", "12", 0); /* octahedron */ + assert_eval_eq("VertexCount[TuranGraph[7,3]]", "7", 0); + assert_eval_eq("EdgeCount[TuranGraph[5,1]]", "0", 0); /* edgeless */ + assert_eval_eq("CompleteGraphQ[TuranGraph[5,5]]", "True", 0); /* K_n */ + assert_eval_eq("RegularGraphQ[TuranGraph[6,3]]", "True", 0); + /* Cross-checks with other builtins. */ + assert_eval_eq("BipartiteGraphQ[TuranGraph[4,2]]", "True", 0); + assert_eval_eq("ChromaticNumber[TuranGraph[4,2]]", "2", 0); + assert_eval_eq("ChromaticNumber[TuranGraph[6,3]]", "3", 0); + /* Bad arguments stay unevaluated. */ + assert_eval_eq("Head[TuranGraph[5,0]]", "TuranGraph", 0); + assert_eval_eq("Head[TuranGraph[5,x]]", "TuranGraph", 0); +} + +static void test_complete_kary_tree(void) { + /* Binary trees (default k=2). */ + assert_eval_eq("VertexCount[CompleteKaryTree[2]]", "3", 0); + assert_eval_eq("VertexCount[CompleteKaryTree[3]]", "7", 0); + assert_eval_eq("EdgeCount[CompleteKaryTree[3]]", "6", 0); + assert_eval_eq("EdgeList[CompleteKaryTree[2]]", "{1 <-> 2, 1 <-> 3}", 0); + assert_eval_eq("TreeGraphQ[CompleteKaryTree[3]]", "True", 0); + /* k-ary: L3 ternary = 13 vertices; root has k children. */ + assert_eval_eq("VertexCount[CompleteKaryTree[3,3]]", "13", 0); + assert_eval_eq("VertexOutDegree[CompleteKaryTree[2,3], 1]", "3", 0); + /* k=1 is a path; L=1 is a single vertex. */ + assert_eval_eq("PathGraphQ[CompleteKaryTree[5,1]]", "True", 0); + assert_eval_eq("VertexCount[CompleteKaryTree[1]]", "1", 0); + assert_eval_eq("EdgeCount[CompleteKaryTree[1]]", "0", 0); + /* Bad arguments stay unevaluated. */ + assert_eval_eq("Head[CompleteKaryTree[0]]", "CompleteKaryTree", 0); + assert_eval_eq("Head[CompleteKaryTree[x]]", "CompleteKaryTree", 0); +} + +static void test_circulant_graph(void) { + /* C_n({1}) is the cycle; single-offset integer form too. */ + assert_eval_eq("EdgeCount[CirculantGraph[5,{1}]]", "5", 0); + assert_eval_eq("EdgeCount[CirculantGraph[6,1]]", "6", 0); + assert_eval_eq("EdgeCount[CirculantGraph[7,{1}]] == EdgeCount[CycleGraph[7]]", "True", 0); + assert_eval_eq("RegularGraphQ[CirculantGraph[5,{1}]]", "True", 0); + /* Full jump set → complete; other regular cases. */ + assert_eval_eq("CompleteGraphQ[CirculantGraph[6,{1,2,3}]]", "True", 0); + assert_eval_eq("RegularGraphQ[CirculantGraph[8,{1,2}]]", "True", 0); + assert_eval_eq("EdgeCount[CirculantGraph[8,{1,2}]]", "16", 0); + /* A jump of n/2 contributes a single matching edge per vertex. */ + assert_eval_eq("EdgeCount[CirculantGraph[6,{3}]]", "3", 0); + assert_eval_eq("VertexCount[CirculantGraph[7,{1,2}]]", "7", 0); + /* Bad arguments stay unevaluated. */ + assert_eval_eq("Head[CirculantGraph[0,{1}]]", "CirculantGraph", 0); + assert_eval_eq("Head[CirculantGraph[5,{x}]]", "CirculantGraph", 0); +} + +static void test_ladder_graph(void) { + assert_eval_eq("EdgeCount[LadderGraph[1]]", "1", 0); /* K2 */ + assert_eval_eq("VertexCount[LadderGraph[1]]", "2", 0); + assert_eval_eq("EdgeCount[LadderGraph[2]]", "4", 0); /* C4 */ + assert_eval_eq("VertexCount[LadderGraph[3]]", "6", 0); + assert_eval_eq("EdgeCount[LadderGraph[3]]", "7", 0); + assert_eval_eq("EdgeCount[LadderGraph[5]] == 3*5-2", "True", 0); + assert_eval_eq("ConnectedGraphQ[LadderGraph[4]]", "True", 0); + assert_eval_eq("BipartiteGraphQ[LadderGraph[4]]", "True", 0); + assert_eval_eq("TreeGraphQ[LadderGraph[3]]", "False", 0); + /* Matches the Cartesian product P_n x P_2. */ + assert_eval_eq("EdgeCount[LadderGraph[3]] == EdgeCount[GraphProduct[PathGraph[3], Graph[{1,2},{1<->2}], \"Cartesian\"]]", "True", 0); + assert_eval_eq("Head[LadderGraph[0]]", "LadderGraph", 0); + assert_eval_eq("Head[LadderGraph[x]]", "LadderGraph", 0); +} + +static void test_cocktail_party_graph(void) { + assert_eval_eq("EdgeCount[CocktailPartyGraph[2]]", "4", 0); /* C4 */ + assert_eval_eq("EdgeCount[CocktailPartyGraph[3]]", "12", 0); /* octahedron */ + assert_eval_eq("VertexCount[CocktailPartyGraph[3]]", "6", 0); + assert_eval_eq("EdgeCount[CocktailPartyGraph[1]]", "0", 0); + assert_eval_eq("EdgeCount[CocktailPartyGraph[4]] == 2*4*3", "True", 0); + assert_eval_eq("RegularGraphQ[CocktailPartyGraph[4]]", "True", 0); + assert_eval_eq("First[DegreeCentrality[CocktailPartyGraph[4]]]", "6", 0); /* 2n-2 */ + assert_eval_eq("ChromaticNumber[CocktailPartyGraph[3]]", "3", 0); + /* Same as the balanced complete n-partite Turán graph on 2n vertices. */ + assert_eval_eq("EdgeCount[CocktailPartyGraph[3]] == EdgeCount[TuranGraph[6,3]]", "True", 0); + assert_eval_eq("Head[CocktailPartyGraph[0]]", "CocktailPartyGraph", 0); + assert_eval_eq("Head[CocktailPartyGraph[x]]", "CocktailPartyGraph", 0); +} + +static void test_kneser_graph(void) { + /* K(5,2) is the Petersen graph. */ + assert_eval_eq("VertexCount[KneserGraph[5,2]]", "10", 0); + assert_eval_eq("EdgeCount[KneserGraph[5,2]]", "15", 0); + assert_eval_eq("RegularGraphQ[KneserGraph[5,2]]", "True", 0); + assert_eval_eq("ConnectedGraphQ[KneserGraph[5,2]]", "True", 0); + assert_eval_eq("BipartiteGraphQ[KneserGraph[5,2]]", "False", 0); + /* K(n,1) = K_n; K(4,2) = perfect matching; vertex labels are subsets. */ + assert_eval_eq("CompleteGraphQ[KneserGraph[5,1]]", "True", 0); + assert_eval_eq("EdgeCount[KneserGraph[4,2]]", "3", 0); + assert_eval_eq("RegularGraphQ[KneserGraph[4,2]]", "True", 0); + assert_eval_eq("First[VertexList[KneserGraph[4,2]]]", "{1, 2}", 0); + assert_eval_eq("VertexCount[KneserGraph[3,0]]", "1", 0); + /* Bad arguments stay unevaluated. */ + assert_eval_eq("Head[KneserGraph[2,3]]", "KneserGraph", 0); + assert_eval_eq("Head[KneserGraph[5,x]]", "KneserGraph", 0); +} + +static void test_generalized_petersen_graph(void) { + /* GP(5,2) is the Petersen graph. */ + assert_eval_eq("VertexCount[GeneralizedPetersenGraph[5,2]]", "10", 0); + assert_eval_eq("EdgeCount[GeneralizedPetersenGraph[5,2]]", "15", 0); + assert_eval_eq("RegularGraphQ[GeneralizedPetersenGraph[5,2]]", "True", 0); + assert_eval_eq("BipartiteGraphQ[GeneralizedPetersenGraph[5,2]]", "False", 0); + assert_eval_eq("EdgeCount[GeneralizedPetersenGraph[5,2]] == EdgeCount[KneserGraph[5,2]]", "True", 0); + /* GP(4,1) is the cube; GP(n,1) is the n-prism (3n edges). */ + assert_eval_eq("VertexCount[GeneralizedPetersenGraph[4,1]]", "8", 0); + assert_eval_eq("EdgeCount[GeneralizedPetersenGraph[4,1]]", "12", 0); + assert_eval_eq("BipartiteGraphQ[GeneralizedPetersenGraph[4,1]]", "True", 0); + assert_eval_eq("EdgeCount[GeneralizedPetersenGraph[6,1]]", "18", 0); + assert_eval_eq("RegularGraphQ[GeneralizedPetersenGraph[3,1]]", "True", 0); + assert_eval_eq("ConnectedGraphQ[GeneralizedPetersenGraph[8,3]]", "True", 0); + /* Bad arguments stay unevaluated. */ + assert_eval_eq("Head[GeneralizedPetersenGraph[2,1]]", "GeneralizedPetersenGraph", 0); + assert_eval_eq("Head[GeneralizedPetersenGraph[5,5]]", "GeneralizedPetersenGraph", 0); +} + +static void test_friendship_graph(void) { + assert_eval_eq("CompleteGraphQ[FriendshipGraph[1]]", "True", 0); /* K3 */ + assert_eval_eq("VertexCount[FriendshipGraph[1]]", "3", 0); + assert_eval_eq("VertexCount[FriendshipGraph[2]]", "5", 0); /* bowtie */ + assert_eval_eq("EdgeCount[FriendshipGraph[2]]", "6", 0); + assert_eval_eq("VertexCount[FriendshipGraph[4]]", "9", 0); /* 2n+1 */ + assert_eval_eq("EdgeCount[FriendshipGraph[4]]", "12", 0); /* 3n */ + assert_eval_eq("First[DegreeCentrality[FriendshipGraph[3]]]", "6", 0); /* hub = 2n */ + assert_eval_eq("DegreeCentrality[FriendshipGraph[3]][[2]]", "2", 0); + assert_eval_eq("ConnectedGraphQ[FriendshipGraph[3]]", "True", 0); + assert_eval_eq("BipartiteGraphQ[FriendshipGraph[2]]", "False", 0); + assert_eval_eq("ChromaticNumber[FriendshipGraph[3]]", "3", 0); + assert_eval_eq("Head[FriendshipGraph[0]]", "FriendshipGraph", 0); +} + +static void test_vertex_coreness(void) { + assert_eval_eq("VertexCoreness[CompleteGraph[4]]", "{3, 3, 3, 3}", 0); + assert_eval_eq("VertexCoreness[CycleGraph[4]]", "{2, 2, 2, 2}", 0); + assert_eval_eq("VertexCoreness[PathGraph[4]]", "{1, 1, 1, 1}", 0); + assert_eval_eq("VertexCoreness[StarGraph[5]]", "{1, 1, 1, 1, 1}", 0); + assert_eval_eq("VertexCoreness[Graph[{1,2,3},{}]]", "{0, 0, 0}", 0); + /* Triangle with a pendant: the triangle is 2-core, the pendant 1-core. */ + assert_eval_eq("VertexCoreness[Graph[{1,2,3,4},{1<->2,2<->3,3<->1,3<->4}]]", "{2, 2, 2, 1}", 0); + /* Direction ignored; max coreness is the degeneracy. */ + assert_eval_eq("VertexCoreness[Graph[{1,2,3},{1->2,2->3,3->1}]]", "{2, 2, 2}", 0); + assert_eval_eq("Max[VertexCoreness[CompleteGraph[4]]]", "3", 0); + /* Coreness ≥ k agrees with membership in KCoreComponents[g, k]. */ + assert_eval_eq("Count[VertexCoreness[Graph[{1,2,3,4},{1<->2,2<->3,3<->1,3<->4}]], _?(#>=2&)]", "3", 0); + assert_eval_eq("Head[VertexCoreness[5]]", "VertexCoreness", 0); +} + +static void test_transitive_reduction(void) { + /* Removes shortcut/transitive edges. */ + assert_eval_eq("EdgeList[TransitiveReductionGraph[Graph[{1,2,3},{1->2,2->3,1->3}]]]", "{1 -> 2, 2 -> 3}", 0); + assert_eval_eq("EdgeCount[TransitiveReductionGraph[Graph[{1,2,3},{1->2,2->3}]]]", "2", 0); + assert_eval_eq("EdgeCount[TransitiveReductionGraph[Graph[{1,2,3,4},{1->2,2->3,3->4,1->4}]]]", "3", 0); + assert_eval_eq("EdgeCount[TransitiveReductionGraph[Graph[{1,2,3},{1->2,1->3,2->3}]]]", "2", 0); + assert_eval_eq("EdgeCount[TransitiveReductionGraph[Graph[{1,2,3,4},{1->2,1->3,2->4,3->4}]]]", "4", 0); + assert_eval_eq("VertexCount[TransitiveReductionGraph[Graph[{1,2,3,4},{1->2,2->3,3->4,1->4}]]]", "4", 0); + /* Reduction preserves reachability (same transitive closure). */ + assert_eval_eq("EdgeCount[TransitiveClosure[TransitiveReductionGraph[Graph[{1,2,3},{1->2,2->3,1->3}]]]] == EdgeCount[TransitiveClosure[Graph[{1,2,3},{1->2,2->3,1->3}]]]", "True", 0); + assert_eval_eq("EdgeCount[TransitiveReductionGraph[Graph[{1,2,3},{}]]]", "0", 0); + /* Cyclic / undirected inputs stay unevaluated. */ + assert_eval_eq("Head[TransitiveReductionGraph[Graph[{1,2,3},{1->2,2->3,3->1}]]]", "TransitiveReductionGraph", 0); + assert_eval_eq("Head[TransitiveReductionGraph[CycleGraph[4]]]", "TransitiveReductionGraph", 0); + assert_eval_eq("Head[TransitiveReductionGraph[5]]", "TransitiveReductionGraph", 0); +} + +static void test_subgraph(void) { + /* Induced subgraph: keep vertices and edges with both endpoints kept. */ + assert_eval_eq("CompleteGraphQ[Subgraph[CompleteGraph[4], {1,2,3}]]", "True", 0); + assert_eval_eq("VertexCount[Subgraph[CompleteGraph[4], {1,2,3}]]", "3", 0); + assert_eval_eq("EdgeCount[Subgraph[CompleteGraph[4], {1,2,3}]]", "3", 0); + assert_eval_eq("EdgeList[Subgraph[CycleGraph[5], {1,2,3}]]", "{1 <-> 2, 2 <-> 3}", 0); + assert_eval_eq("VertexCount[Subgraph[CompleteGraph[4], {}]]", "0", 0); + assert_eval_eq("EdgeCount[Subgraph[CompleteGraph[4], {1}]]", "0", 0); + /* Order preserved, non-vertices ignored, duplicates collapsed. */ + assert_eval_eq("VertexList[Subgraph[CompleteGraph[4], {3,1}]]", "{3, 1}", 0); + assert_eval_eq("VertexList[Subgraph[CompleteGraph[3], {1,2,9}]]", "{1, 2}", 0); + assert_eval_eq("VertexCount[Subgraph[CompleteGraph[3], {1,1,2}]]", "2", 0); + assert_eval_eq("EdgeList[Subgraph[Graph[{1,2,3},{1->2,2->3,1->3}], {1,2}]]", "{1 -> 2}", 0); + assert_eval_eq("Head[Subgraph[5, {1}]]", "Subgraph", 0); +} + +static void test_vertex_delete(void) { + assert_eval_eq("CompleteGraphQ[VertexDelete[CompleteGraph[4], 1]]", "True", 0); + assert_eval_eq("VertexList[VertexDelete[CompleteGraph[4], 1]]", "{2, 3, 4}", 0); + assert_eval_eq("EdgeCount[VertexDelete[CompleteGraph[4], {1,2}]]", "1", 0); + assert_eval_eq("VertexCount[VertexDelete[CompleteGraph[4], {1,2}]]", "2", 0); + assert_eval_eq("EdgeCount[VertexDelete[PathGraph[3], 2]]", "0", 0); + assert_eval_eq("VertexList[VertexDelete[PathGraph[3], 2]]", "{1, 3}", 0); + assert_eval_eq("EdgeCount[VertexDelete[CycleGraph[4], 1]]", "2", 0); + assert_eval_eq("EdgeList[VertexDelete[Graph[{1,2,3},{1->2,2->3}], 1]]", "{2 -> 3}", 0); + assert_eval_eq("VertexCount[VertexDelete[CompleteGraph[3], 9]]", "3", 0); /* non-vertex: unchanged */ + assert_eval_eq("VertexCount[VertexDelete[CompleteGraph[3], {1,2,3}]]", "0", 0); + assert_eval_eq("Head[VertexDelete[5, 1]]", "VertexDelete", 0); +} + +static void test_edge_delete(void) { + /* Delete via <-> sugar; all vertices kept; matching is symmetric. */ + assert_eval_eq("EdgeCount[EdgeDelete[CycleGraph[4], 1<->2]]", "3", 0); + assert_eval_eq("VertexCount[EdgeDelete[CycleGraph[4], 1<->2]]", "4", 0); + assert_eval_eq("EdgeCount[EdgeDelete[CycleGraph[4], 2<->1]]", "3", 0); + assert_eval_eq("EdgeCount[EdgeDelete[CycleGraph[4], UndirectedEdge[1,2]]]", "3", 0); + assert_eval_eq("EdgeList[EdgeDelete[CycleGraph[4], 1<->2]]", "{2 <-> 3, 3 <-> 4, 4 <-> 1}", 0); + /* Delete a list of edges; nonexistent edges are ignored. */ + assert_eval_eq("EdgeCount[EdgeDelete[CompleteGraph[4], {1<->2,3<->4}]]", "4", 0); + assert_eval_eq("EdgeCount[EdgeDelete[PathGraph[3], 1<->3]]", "2", 0); + /* Directed: delete via ->, not symmetric. */ + assert_eval_eq("EdgeList[EdgeDelete[Graph[{1,2,3},{1->2,2->3}], 1->2]]", "{2 -> 3}", 0); + assert_eval_eq("EdgeCount[EdgeDelete[Graph[{1,2},{1->2}], 2->1]]", "1", 0); + assert_eval_eq("GraphQ[EdgeDelete[CompleteGraph[4], 1<->2]]", "True", 0); + assert_eval_eq("Head[EdgeDelete[5, 1<->2]]", "EdgeDelete", 0); +} + +static void test_edge_add(void) { + /* Adding a chord closes a path into a triangle. */ + assert_eval_eq("EdgeCount[EdgeAdd[PathGraph[3], 1<->3]]", "3", 0); + assert_eval_eq("CompleteGraphQ[EdgeAdd[PathGraph[3], 1<->3]]", "True", 0); + /* Existing / symmetric-existing edges are not duplicated. */ + assert_eval_eq("EdgeCount[EdgeAdd[CycleGraph[4], 1<->2]]", "4", 0); + assert_eval_eq("EdgeCount[EdgeAdd[CycleGraph[4], 2<->1]]", "4", 0); + /* Missing endpoints become new vertices. */ + assert_eval_eq("VertexCount[EdgeAdd[Graph[{1,2},{1<->2}], 2<->3]]", "3", 0); + assert_eval_eq("EdgeCount[EdgeAdd[Graph[{1,2},{1<->2}], 2<->3]]", "2", 0); + /* Self-loops skipped; list add; directed via ->. */ + assert_eval_eq("EdgeCount[EdgeAdd[PathGraph[3], 1<->1]]", "2", 0); + assert_eval_eq("EdgeCount[EdgeAdd[Graph[{1,2,3,4},{}], {1<->2,3<->4}]]", "2", 0); + assert_eval_eq("MemberQ[EdgeList[EdgeAdd[Graph[{1,2},{1->2}], 2->1]], DirectedEdge[2,1]]", "True", 0); + assert_eval_eq("EdgeCount[EdgeAdd[Graph[{1,2},{1->2}], 2->1]]", "2", 0); + assert_eval_eq("GraphQ[EdgeAdd[PathGraph[3], 1<->3]]", "True", 0); + assert_eval_eq("Head[EdgeAdd[5, 1<->2]]", "EdgeAdd", 0); +} + +static void test_vertex_add(void) { + assert_eval_eq("VertexCount[VertexAdd[CompleteGraph[3], 4]]", "4", 0); + assert_eval_eq("EdgeCount[VertexAdd[CompleteGraph[3], 4]]", "3", 0); /* isolated */ + assert_eval_eq("VertexList[VertexAdd[CompleteGraph[3], 4]]", "{1, 2, 3, 4}", 0); + assert_eval_eq("VertexCount[VertexAdd[Graph[{1,2},{1<->2}], {3,4}]]", "4", 0); + assert_eval_eq("VertexCount[VertexAdd[CompleteGraph[3], 2]]", "3", 0); /* duplicate ignored */ + assert_eval_eq("VertexList[VertexAdd[Graph[{1},{}], x]]", "{1, x}", 0); + assert_eval_eq("Last[DegreeCentrality[VertexAdd[CompleteGraph[3], 4]]]", "0", 0); + assert_eval_eq("GraphQ[VertexAdd[CompleteGraph[3], 4]]", "True", 0); + assert_eval_eq("VertexCount[VertexAdd[Graph[{},{}], {1,2}]]", "2", 0); + assert_eval_eq("Head[VertexAdd[5, 1]]", "VertexAdd", 0); +} + +static void test_neighborhood_graph(void) { + assert_eval_eq("CompleteGraphQ[NeighborhoodGraph[CompleteGraph[4], 1]]", "True", 0); + assert_eval_eq("Sort[VertexList[NeighborhoodGraph[CycleGraph[5], 1]]]", "{1, 2, 5}", 0); + assert_eval_eq("EdgeCount[NeighborhoodGraph[CycleGraph[5], 1]]", "2", 0); + assert_eval_eq("VertexCount[NeighborhoodGraph[PathGraph[5], 3]]", "3", 0); + assert_eval_eq("EdgeCount[NeighborhoodGraph[PathGraph[5], 3]]", "2", 0); + assert_eval_eq("MemberQ[VertexList[NeighborhoodGraph[CycleGraph[5], 1]], 1]", "True", 0); + /* k controls the radius: 0 → just v, larger k widens. */ + assert_eval_eq("VertexCount[NeighborhoodGraph[CompleteGraph[4], 1, 0]]", "1", 0); + assert_eval_eq("EdgeCount[NeighborhoodGraph[CompleteGraph[4], 1, 0]]", "0", 0); + assert_eval_eq("VertexCount[NeighborhoodGraph[PathGraph[5], 3, 2]]", "5", 0); + /* Bad arguments stay unevaluated. */ + assert_eval_eq("Head[NeighborhoodGraph[CycleGraph[3], 9]]", "NeighborhoodGraph", 0); + assert_eval_eq("Head[NeighborhoodGraph[5, 1]]", "NeighborhoodGraph", 0); +} + +static void test_graph_disjoint_union(void) { + assert_eval_eq("VertexCount[GraphDisjointUnion[CompleteGraph[3], CompleteGraph[3]]]", "6", 0); + assert_eval_eq("EdgeCount[GraphDisjointUnion[CompleteGraph[3], CompleteGraph[3]]]", "6", 0); + assert_eval_eq("Length[ConnectedComponents[GraphDisjointUnion[CompleteGraph[3], CompleteGraph[3]]]]", "2", 0); + assert_eval_eq("VertexCount[GraphDisjointUnion[PathGraph[2], PathGraph[2]]]", "4", 0); + assert_eval_eq("EdgeCount[GraphDisjointUnion[PathGraph[2], PathGraph[2]]]", "2", 0); + assert_eval_eq("VertexList[GraphDisjointUnion[Graph[{a,b},{a<->b}], Graph[{x},{}]]]", "{1, 2, 3}", 0); + assert_eval_eq("ConnectedGraphQ[GraphDisjointUnion[CompleteGraph[3], CompleteGraph[3]]]", "False", 0); + assert_eval_eq("EdgeCount[GraphDisjointUnion[CycleGraph[4], PathGraph[3]]] == 4+2", "True", 0); + /* No cross edges (unlike GraphJoin); directed edges preserved. */ + assert_eval_eq("EdgeCount[GraphDisjointUnion[CompleteGraph[2], CompleteGraph[2]]] < EdgeCount[GraphJoin[CompleteGraph[2], CompleteGraph[2]]]", "True", 0); + assert_eval_eq("DirectedGraphQ[GraphDisjointUnion[Graph[{1,2},{1->2}], Graph[{1,2},{1->2}]]]", "True", 0); + assert_eval_eq("Head[GraphDisjointUnion[5, CycleGraph[3]]]", "GraphDisjointUnion", 0); +} + +static void test_edge_contract(void) { + /* Contracting a triangle edge leaves a single edge. */ + assert_eval_eq("EdgeList[EdgeContract[Graph[{1,2,3},{1<->2,2<->3,3<->1}], 1<->2]]", "{1 <-> 3}", 0); + assert_eval_eq("VertexList[EdgeContract[Graph[{1,2,3},{1<->2,2<->3,3<->1}], 1<->2]]", "{1, 3}", 0); + assert_eval_eq("EdgeCount[EdgeContract[PathGraph[3], 1<->2]]", "1", 0); + /* List form; equals the corresponding VertexContract. */ + assert_eval_eq("VertexCount[EdgeContract[CompleteGraph[4], {1,2}]]", "3", 0); + assert_eval_eq("EdgeContract[CompleteGraph[4], 1<->2] === VertexContract[CompleteGraph[4], {1,2}]", "True", 0); + assert_eval_eq("VertexCount[EdgeContract[CompleteGraph[4], 1<->2]]", "3", 0); + assert_eval_eq("EdgeCount[EdgeContract[CompleteGraph[4], 1<->2]]", "3", 0); + assert_eval_eq("EdgeCount[EdgeContract[Graph[{1,2,3},{1->2,2->3}], 1->2]]", "1", 0); + /* Bad edge specs stay unevaluated. */ + assert_eval_eq("Head[EdgeContract[CycleGraph[3], 1<->1]]", "EdgeContract", 0); + assert_eval_eq("Head[EdgeContract[CycleGraph[3], 1<->9]]", "EdgeContract", 0); + assert_eval_eq("Head[EdgeContract[5, 1<->2]]", "EdgeContract", 0); +} + +static void test_find_matching(void) { + assert_eval_eq("Length[FindIndependentEdgeSet[CompleteGraph[4]]]", "2", 0); + assert_eval_eq("Length[FindIndependentEdgeSet[CompleteGraph[6]]]", "3", 0); /* perfect */ + assert_eval_eq("Length[FindIndependentEdgeSet[PathGraph[4]]]", "2", 0); + assert_eval_eq("Length[FindIndependentEdgeSet[CycleGraph[4]]]", "2", 0); + assert_eval_eq("Length[FindIndependentEdgeSet[CycleGraph[5]]]", "2", 0); + assert_eval_eq("Length[FindIndependentEdgeSet[StarGraph[5]]]", "1", 0); + assert_eval_eq("Length[FindIndependentEdgeSet[CompleteGraph[3]]]", "1", 0); + assert_eval_eq("FindIndependentEdgeSet[Graph[{1,2,3},{}]]", "{}", 0); + /* Independence: the matching covers 2*size distinct vertices. */ + assert_eval_eq("With[{mm=FindIndependentEdgeSet[PathGraph[4]]}, Length[Union[Flatten[mm/.UndirectedEdge->List]]] == 2*Length[mm]]", "True", 0); + assert_eval_eq("Head[FindIndependentEdgeSet[5]]", "FindIndependentEdgeSet", 0); +} + +static void test_find_dominating_set(void) { + assert_eval_eq("FindDominatingSet[StarGraph[5]]", "{1}", 0); + assert_eval_eq("Length[FindDominatingSet[CompleteGraph[5]]]", "1", 0); + assert_eval_eq("Length[FindDominatingSet[PathGraph[4]]]", "2", 0); + assert_eval_eq("Length[FindDominatingSet[CycleGraph[4]]]", "2", 0); + assert_eval_eq("Length[FindDominatingSet[CycleGraph[6]]]", "2", 0); + assert_eval_eq("Length[FindDominatingSet[CycleGraph[7]]]", "3", 0); + assert_eval_eq("Length[FindDominatingSet[Graph[{1,2,3},{}]]]", "3", 0); /* edgeless */ + assert_eval_eq("FindDominatingSet[Graph[{1},{}]]", "{1}", 0); + assert_eval_eq("Length[FindDominatingSet[Graph[{1,2,3},{1->2,2->3,3->1}]]]", "1", 0); + assert_eval_eq("Head[FindDominatingSet[5]]", "FindDominatingSet", 0); +} + +static void test_find_edge_cover(void) { + assert_eval_eq("Length[FindEdgeCover[PathGraph[4]]]", "2", 0); + assert_eval_eq("Length[FindEdgeCover[StarGraph[5]]]", "4", 0); /* every leaf edge */ + assert_eval_eq("Length[FindEdgeCover[CompleteGraph[4]]]", "2", 0); + assert_eval_eq("Length[FindEdgeCover[CompleteGraph[3]]]", "2", 0); + assert_eval_eq("Length[FindEdgeCover[CycleGraph[6]]]", "3", 0); + assert_eval_eq("Length[FindEdgeCover[Graph[{1,2},{1<->2}]]]", "1", 0); + /* No cover when there is an isolated vertex. */ + assert_eval_eq("FindEdgeCover[Graph[{1,2},{}]]", "{}", 0); + /* Gallai: |min edge cover| = n - |max matching|; cover touches every vertex. */ + assert_eval_eq("Length[FindEdgeCover[PathGraph[5]]] == 5 - Length[FindIndependentEdgeSet[PathGraph[5]]]", "True", 0); + assert_eval_eq("With[{ec=FindEdgeCover[CycleGraph[5]]}, Length[Union[Flatten[ec/.UndirectedEdge->List]]] == 5]", "True", 0); + assert_eval_eq("Head[FindEdgeCover[5]]", "FindEdgeCover", 0); +} + +static void test_find_vertex_coloring(void) { + assert_eval_eq("FindVertexColoring[CompleteGraph[3]]", "{1, 2, 3}", 0); + assert_eval_eq("FindVertexColoring[CycleGraph[4]]", "{1, 2, 1, 2}", 0); + assert_eval_eq("FindVertexColoring[Graph[{1,2,3},{}]]", "{1, 1, 1}", 0); + assert_eval_eq("FindVertexColoring[Graph[{1},{}]]", "{1}", 0); + assert_eval_eq("Length[FindVertexColoring[CycleGraph[6]]]", "6", 0); + /* Number of colours used equals the chromatic number. */ + assert_eval_eq("Max[FindVertexColoring[CycleGraph[5]]] == ChromaticNumber[CycleGraph[5]]", "True", 0); + assert_eval_eq("Max[FindVertexColoring[CompleteGraph[4]]] == 4", "True", 0); + assert_eval_eq("Max[FindVertexColoring[PathGraph[5]]]", "2", 0); + assert_eval_eq("Max[FindVertexColoring[StarGraph[5]]]", "2", 0); + assert_eval_eq("Length[Union[FindVertexColoring[CompleteGraph[4]]]]", "4", 0); + assert_eval_eq("Head[FindVertexColoring[5]]", "FindVertexColoring", 0); +} + +static void test_graph_assortativity(void) { + /* Stars are perfectly disassortative; a path P4 is -1/2. */ + assert_eval_eq("GraphAssortativity[StarGraph[4]]", "-1", 0); + assert_eval_eq("GraphAssortativity[StarGraph[5]]", "-1", 0); + assert_eval_eq("GraphAssortativity[PathGraph[4]]", "-1/2", 0); + /* Regular / edgeless graphs have Indeterminate assortativity (zero variance). */ + assert_eval_eq("GraphAssortativity[CycleGraph[5]]", "Indeterminate", 0); + assert_eval_eq("GraphAssortativity[CompleteGraph[4]]", "Indeterminate", 0); + assert_eval_eq("GraphAssortativity[Graph[{1,2,3},{}]]", "Indeterminate", 0); + assert_eval_eq("GraphAssortativity[Graph[{1,2,3},{1->2,2->3,3->1}]]", "Indeterminate", 0); + assert_eval_eq("With[{r=GraphAssortativity[PathGraph[5]]}, -1<=r<=1]", "True", 0); + assert_eval_eq("Head[GraphAssortativity[5]]", "GraphAssortativity", 0); +} + +static void test_incidence_list(void) { + assert_eval_eq("IncidenceList[CycleGraph[4], 1]", "{1 <-> 2, 4 <-> 1}", 0); + assert_eval_eq("Length[IncidenceList[CycleGraph[4], 1]]", "2", 0); + assert_eval_eq("Length[IncidenceList[StarGraph[5], 1]]", "4", 0); + assert_eval_eq("Length[IncidenceList[StarGraph[5], 2]]", "1", 0); + assert_eval_eq("IncidenceList[CycleGraph[3], 9]", "{}", 0); + assert_eval_eq("IncidenceList[Graph[{1,2},{}], 1]", "{}", 0); + /* Directed: includes both in- and out-edges at the vertex. */ + assert_eval_eq("Length[IncidenceList[Graph[{1,2,3},{1->2,2->3}], 2]]", "2", 0); + assert_eval_eq("IncidenceList[Graph[{1,2,3},{1->2,2->3}], 1]", "{1 -> 2}", 0); + /* Incidence count matches the (undirected) vertex degree. */ + assert_eval_eq("Length[IncidenceList[CompleteGraph[4], 1]] == VertexDegree[CompleteGraph[4], 1]", "True", 0); + assert_eval_eq("Head[IncidenceList[5, 1]]", "IncidenceList", 0); +} + +static void test_vertex_components(void) { + /* Directed path 1->2->3. */ + assert_eval_eq("VertexOutComponent[Graph[{1,2,3},{1->2,2->3}], 1]", "{1, 2, 3}", 0); + assert_eval_eq("VertexOutComponent[Graph[{1,2,3},{1->2,2->3}], 2]", "{2, 3}", 0); + assert_eval_eq("VertexOutComponent[Graph[{1,2,3},{1->2,2->3}], 3]", "{3}", 0); + assert_eval_eq("VertexInComponent[Graph[{1,2,3},{1->2,2->3}], 3]", "{1, 2, 3}", 0); + assert_eval_eq("VertexInComponent[Graph[{1,2,3},{1->2,2->3}], 1]", "{1}", 0); + assert_eval_eq("VertexInComponent[Graph[{1,2,3},{1->2,2->3}], 2]", "{1, 2}", 0); + /* Undirected: both give v's connected component. */ + assert_eval_eq("VertexOutComponent[CycleGraph[4], 1]", "{1, 2, 3, 4}", 0); + assert_eval_eq("VertexInComponent[CycleGraph[4], 1]", "{1, 2, 3, 4}", 0); + assert_eval_eq("VertexOutComponent[Graph[{1,2,3},{1<->2}], 1]", "{1, 2}", 0); + /* Directed cycle: every vertex reaches every other. */ + assert_eval_eq("VertexOutComponent[Graph[{1,2,3},{1->2,2->3,3->1}], 2]", "{1, 2, 3}", 0); + assert_eval_eq("Head[VertexOutComponent[CycleGraph[3], 9]]", "VertexOutComponent", 0); + assert_eval_eq("Head[VertexInComponent[5, 1]]", "VertexInComponent", 0); +} + +static void test_graph_periphery(void) { + /* Periphery = eccentricity-maximizing vertices (dual of GraphCenter). */ + assert_eval_eq("GraphPeriphery[PathGraph[5]]", "{1, 5}", 0); + assert_eval_eq("GraphCenter[PathGraph[5]]", "{3}", 0); + assert_eval_eq("GraphPeriphery[PathGraph[6]]", "{1, 6}", 0); + assert_eval_eq("Length[GraphPeriphery[PathGraph[4]]]", "2", 0); + assert_eval_eq("GraphPeriphery[StarGraph[5]]", "{2, 3, 4, 5}", 0); + /* Vertex-transitive graphs: every vertex is peripheral. */ + assert_eval_eq("GraphPeriphery[CycleGraph[5]]", "{1, 2, 3, 4, 5}", 0); + assert_eval_eq("GraphPeriphery[CompleteGraph[4]]", "{1, 2, 3, 4}", 0); + assert_eval_eq("GraphPeriphery[Graph[{1},{}]]", "{1}", 0); + /* Disconnected: all vertices have infinite eccentricity. */ + assert_eval_eq("GraphPeriphery[Graph[{1,2,3},{1<->2}]]", "{1, 2, 3}", 0); + assert_eval_eq("Head[GraphPeriphery[5]]", "GraphPeriphery", 0); +} + +static void test_antiprism_graph(void) { + /* A3 is the octahedron: 6 vertices, 12 edges, 4-regular. */ + assert_eval_eq("VertexCount[AntiprismGraph[3]]", "6", 0); + assert_eval_eq("EdgeCount[AntiprismGraph[3]]", "12", 0); + assert_eval_eq("RegularGraphQ[AntiprismGraph[3]]", "True", 0); + assert_eval_eq("EdgeCount[AntiprismGraph[3]] == EdgeCount[TuranGraph[6,3]]", "True", 0); + assert_eval_eq("VertexCount[AntiprismGraph[4]]", "8", 0); + assert_eval_eq("EdgeCount[AntiprismGraph[4]]", "16", 0); + assert_eval_eq("EdgeCount[AntiprismGraph[5]] == 4*5", "True", 0); + assert_eval_eq("RegularGraphQ[AntiprismGraph[5]]", "True", 0); + assert_eval_eq("First[DegreeCentrality[AntiprismGraph[4]]]", "4", 0); + assert_eval_eq("ConnectedGraphQ[AntiprismGraph[4]]", "True", 0); + assert_eval_eq("Head[AntiprismGraph[2]]", "AntiprismGraph", 0); +} + +static void test_prism_graph(void) { + assert_eval_eq("VertexCount[PrismGraph[3]]", "6", 0); + assert_eval_eq("EdgeCount[PrismGraph[3]]", "9", 0); + assert_eval_eq("RegularGraphQ[PrismGraph[3]]", "True", 0); + assert_eval_eq("EdgeCount[PrismGraph[4]]", "12", 0); /* cube */ + assert_eval_eq("BipartiteGraphQ[PrismGraph[4]]", "True", 0); + assert_eval_eq("EdgeCount[PrismGraph[5]] == 3*5", "True", 0); + assert_eval_eq("First[DegreeCentrality[PrismGraph[5]]]", "3", 0); + assert_eval_eq("ConnectedGraphQ[PrismGraph[6]]", "True", 0); + /* Isomorphic to GeneralizedPetersenGraph[n,1] (same edge count). */ + assert_eval_eq("EdgeCount[PrismGraph[5]] == EdgeCount[GeneralizedPetersenGraph[5,1]]", "True", 0); + assert_eval_eq("Head[PrismGraph[2]]", "PrismGraph", 0); +} + +static void test_sunlet_graph(void) { + assert_eval_eq("VertexCount[SunletGraph[3]]", "6", 0); + assert_eval_eq("EdgeCount[SunletGraph[3]]", "6", 0); + assert_eval_eq("VertexCount[SunletGraph[5]]", "10", 0); + assert_eval_eq("EdgeCount[SunletGraph[5]] == 2*5", "True", 0); + assert_eval_eq("RegularGraphQ[SunletGraph[4]]", "False", 0); + assert_eval_eq("First[DegreeCentrality[SunletGraph[4]]]", "3", 0); /* cycle vertex */ + assert_eval_eq("Last[DegreeCentrality[SunletGraph[4]]]", "1", 0); /* pendant */ + assert_eval_eq("ConnectedGraphQ[SunletGraph[5]]", "True", 0); + assert_eval_eq("BipartiteGraphQ[SunletGraph[4]]", "True", 0); + assert_eval_eq("BipartiteGraphQ[SunletGraph[3]]", "False", 0); + assert_eval_eq("Head[SunletGraph[2]]", "SunletGraph", 0); +} + +static void test_helm_graph(void) { + assert_eval_eq("VertexCount[HelmGraph[3]]", "7", 0); + assert_eval_eq("EdgeCount[HelmGraph[3]]", "9", 0); + assert_eval_eq("VertexCount[HelmGraph[5]]", "11", 0); + assert_eval_eq("EdgeCount[HelmGraph[5]] == 3*5", "True", 0); + assert_eval_eq("First[DegreeCentrality[HelmGraph[4]]]", "4", 0); /* hub */ + assert_eval_eq("DegreeCentrality[HelmGraph[4]][[2]]", "4", 0); /* rim */ + assert_eval_eq("Last[DegreeCentrality[HelmGraph[4]]]", "1", 0); /* pendant */ + assert_eval_eq("ConnectedGraphQ[HelmGraph[5]]", "True", 0); + assert_eval_eq("RegularGraphQ[HelmGraph[4]]", "False", 0); + assert_eval_eq("BipartiteGraphQ[HelmGraph[4]]", "False", 0); + assert_eval_eq("Head[HelmGraph[2]]", "HelmGraph", 0); +} + +static void test_gear_graph(void) { + assert_eval_eq("VertexCount[GearGraph[3]]", "7", 0); + assert_eval_eq("EdgeCount[GearGraph[3]]", "9", 0); + assert_eval_eq("VertexCount[GearGraph[5]]", "11", 0); + assert_eval_eq("EdgeCount[GearGraph[5]] == 3*5", "True", 0); + /* Gear graphs are bipartite (χ = 2). */ + assert_eval_eq("BipartiteGraphQ[GearGraph[4]]", "True", 0); + assert_eval_eq("BipartiteGraphQ[GearGraph[3]]", "True", 0); + assert_eval_eq("ChromaticNumber[GearGraph[4]]", "2", 0); + assert_eval_eq("First[DegreeCentrality[GearGraph[4]]]", "4", 0); /* hub degree n */ + assert_eval_eq("ConnectedGraphQ[GearGraph[5]]", "True", 0); + assert_eval_eq("RegularGraphQ[GearGraph[4]]", "False", 0); + assert_eval_eq("Head[GearGraph[2]]", "GearGraph", 0); +} + +static void test_edge_betweenness(void) { + assert_eval_eq("EdgeBetweennessCentrality[PathGraph[3]]", "{2, 2}", 0); + assert_eval_eq("EdgeBetweennessCentrality[PathGraph[4]]", "{3, 4, 3}", 0); + assert_eval_eq("EdgeBetweennessCentrality[StarGraph[4]]", "{3, 3, 3}", 0); + assert_eval_eq("EdgeBetweennessCentrality[CycleGraph[4]]", "{2, 2, 2, 2}", 0); + assert_eval_eq("EdgeBetweennessCentrality[CycleGraph[5]]", "{3, 3, 3, 3, 3}", 0); + assert_eval_eq("EdgeBetweennessCentrality[CompleteGraph[4]]", "{1, 1, 1, 1, 1, 1}", 0); + assert_eval_eq("EdgeBetweennessCentrality[Graph[{1,2},{1<->2}]]", "{1}", 0); + assert_eval_eq("EdgeBetweennessCentrality[Graph[{1,2,3},{}]]", "{}", 0); + assert_eval_eq("EdgeBetweennessCentrality[Graph[{1,2,3},{1->2,2->3}]]", "{2, 2}", 0); + assert_eval_eq("Length[EdgeBetweennessCentrality[CompleteGraph[5]]]", "10", 0); + assert_eval_eq("Head[EdgeBetweennessCentrality[5]]", "EdgeBetweennessCentrality", 0); +} + +static void test_dodecahedral_graph(void) { + assert_eval_eq("VertexCount[DodecahedralGraph[]]", "20", 0); + assert_eval_eq("EdgeCount[DodecahedralGraph[]]", "30", 0); + assert_eval_eq("RegularGraphQ[DodecahedralGraph[]]", "True", 0); + assert_eval_eq("First[DegreeCentrality[DodecahedralGraph[]]]", "3", 0); + assert_eval_eq("ConnectedGraphQ[DodecahedralGraph[]]", "True", 0); + assert_eval_eq("BipartiteGraphQ[DodecahedralGraph[]]", "False", 0); + assert_eval_eq("ChromaticNumber[DodecahedralGraph[]]", "3", 0); + assert_eval_eq("HamiltonianGraphQ[DodecahedralGraph[]]", "True", 0); + assert_eval_eq("EdgeCount[DodecahedralGraph[]] == EdgeCount[GeneralizedPetersenGraph[10,2]]", "True", 0); + assert_eval_eq("Head[DodecahedralGraph[5]]", "DodecahedralGraph", 0); +} + +static void test_icosahedral_graph(void) { + assert_eval_eq("VertexCount[IcosahedralGraph[]]", "12", 0); + assert_eval_eq("EdgeCount[IcosahedralGraph[]]", "30", 0); + assert_eval_eq("RegularGraphQ[IcosahedralGraph[]]", "True", 0); + assert_eval_eq("Union[DegreeCentrality[IcosahedralGraph[]]]", "{5}", 0); /* 5-regular */ + assert_eval_eq("ConnectedGraphQ[IcosahedralGraph[]]", "True", 0); + assert_eval_eq("BipartiteGraphQ[IcosahedralGraph[]]", "False", 0); + assert_eval_eq("ChromaticNumber[IcosahedralGraph[]]", "4", 0); + assert_eval_eq("HamiltonianGraphQ[IcosahedralGraph[]]", "True", 0); + assert_eval_eq("Head[IcosahedralGraph[5]]", "IcosahedralGraph", 0); +} + +int main(void) { + symtab_init(); + core_init(); + + TEST(test_edge_sugar_normalization); + TEST(test_vertex_derivation); + TEST(test_summary_printing); + TEST(test_graphq); + TEST(test_rejections); + TEST(test_inputform_roundtrip); + TEST(test_query_builtins); + TEST(test_query_undirected); + TEST(test_matrix_views); + TEST(test_generators); + TEST(test_random_graph); + TEST(test_shortest_path); + TEST(test_components); + TEST(test_spanning_and_connectivity); + TEST(test_graphplot); + TEST(test_graphplot_options); + TEST(test_highlight_graph); + TEST(test_graph3d); + TEST(test_bipartite); + TEST(test_metrics); + TEST(test_acyclic); + TEST(test_complement); + TEST(test_kirchhoff); + TEST(test_edge_connectivity); + TEST(test_line_graph); + TEST(test_eulerian); + TEST(test_star_wheel); + TEST(test_complete_multipartite); + TEST(test_grid_hypercube); + TEST(test_closeness); + TEST(test_transitive_closure); + TEST(test_betweenness); + TEST(test_find_eulerian); + TEST(test_find_hamiltonian); + TEST(test_graph_power); + TEST(test_find_cycle); + TEST(test_graph_distance_matrix); + TEST(test_graph_density); + TEST(test_degree_centrality); + TEST(test_find_hamiltonian_path); + TEST(test_kcore_components); + TEST(test_local_clustering); + TEST(test_global_clustering); + TEST(test_mean_clustering); + TEST(test_find_clique); + TEST(test_find_independent); + TEST(test_find_vertex_cover); + TEST(test_graph_reciprocity); + TEST(test_chromatic_polynomial); + TEST(test_chromatic_number); + TEST(test_degree_sequence); + TEST(test_tree_graph_q); + TEST(test_strongly_connected_q); + TEST(test_hamiltonian_graph_q); + TEST(test_regular_graph_q); + TEST(test_complete_graph_q); + TEST(test_graph_union); + TEST(test_graph_intersection); + TEST(test_graph_difference); + TEST(test_graph_reverse); + TEST(test_path_graph_q); + TEST(test_vertex_contract); + TEST(test_pagerank_centrality); + TEST(test_katz_centrality); + TEST(test_graph_join); + TEST(test_index_graph); + TEST(test_empty_and_mixed_q); + TEST(test_graph_product); + TEST(test_turan_graph); + TEST(test_complete_kary_tree); + TEST(test_circulant_graph); + TEST(test_ladder_graph); + TEST(test_cocktail_party_graph); + TEST(test_kneser_graph); + TEST(test_generalized_petersen_graph); + TEST(test_friendship_graph); + TEST(test_vertex_coreness); + TEST(test_transitive_reduction); + TEST(test_subgraph); + TEST(test_vertex_delete); + TEST(test_edge_delete); + TEST(test_edge_add); + TEST(test_vertex_add); + TEST(test_neighborhood_graph); + TEST(test_graph_disjoint_union); + TEST(test_edge_contract); + TEST(test_find_matching); + TEST(test_find_dominating_set); + TEST(test_find_edge_cover); + TEST(test_find_vertex_coloring); + TEST(test_graph_assortativity); + TEST(test_incidence_list); + TEST(test_vertex_components); + TEST(test_graph_periphery); + TEST(test_antiprism_graph); + TEST(test_prism_graph); + TEST(test_sunlet_graph); + TEST(test_helm_graph); + TEST(test_gear_graph); + TEST(test_edge_betweenness); + TEST(test_dodecahedral_graph); + TEST(test_icosahedral_graph); + + printf("All graph tests passed!\n"); + return 0; +}