Skip to content
This repository was archived by the owner on Jun 18, 2026. It is now read-only.

Commit 378ae2b

Browse files
tests(TreewidthAnalyzer): 19-case suite pinning bounds + tree-decomposition invariants
Adds gvisual.TreewidthAnalyzerTest covering the canonical graph families where treewidth is known exactly (empty, singleton, paths, caterpillars, cycles C3/C5/C8, complete K3/K4/K6, two disconnected triangles, independent sets) and bracket-only cases (3x4 grid, deterministic dense random graph) where the heuristics only give bounds. Each test also verifies the structural invariants of the produced TreeDecomposition: every graph vertex appears in at least one bag, every graph edge has both endpoints in some shared bag, and the reported width equals max(|bag|)-1. Bag/TreeEdge value semantics (unmodifiable vertex set, toString format, getters) are covered separately. Wires the new test into .github/workflows/ci.yml so both JDK 11 and JDK 17 matrix jobs run it. Local run: 19 tests pass; full CI runner set (15 classes, 347 tests) still green.
1 parent 300f31a commit 378ae2b

2 files changed

Lines changed: 380 additions & 1 deletion

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,4 +84,5 @@ jobs:
8484
gvisual.GraphPowerCalculatorTest \
8585
gvisual.WienerIndexCalculatorTest \
8686
gvisual.GraphIsomorphismCheckerTest \
87-
gvisual.GraphFileParserTest
87+
gvisual.GraphFileParserTest \
88+
gvisual.TreewidthAnalyzerTest
Lines changed: 378 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,378 @@
1+
package gvisual;
2+
3+
import edu.uci.ics.jung.graph.Graph;
4+
import edu.uci.ics.jung.graph.UndirectedSparseGraph;
5+
import org.junit.Before;
6+
import org.junit.Test;
7+
8+
import java.util.HashSet;
9+
import java.util.Set;
10+
11+
import static org.junit.Assert.*;
12+
13+
/**
14+
* Tests for {@link TreewidthAnalyzer}.
15+
*
16+
* <p>These tests pin down the analyzer on canonical graph families where the
17+
* treewidth is known exactly:
18+
* <ul>
19+
* <li>Empty graph and singleton: treewidth 0</li>
20+
* <li>Forests (paths, trees): treewidth 1</li>
21+
* <li>Cycle C_n (n &ge; 3): treewidth 2</li>
22+
* <li>Complete graph K_n: treewidth n-1 (both bounds tight, exact)</li>
23+
* <li>n-by-m grid: treewidth = min(n, m) — but the heuristics here only
24+
* give bounds, so we assert the bounds bracket the true value</li>
25+
* </ul>
26+
*
27+
* <p>Beyond exact-value pinning, these tests guard the structural
28+
* invariants of the produced tree decomposition: every graph vertex
29+
* appears in at least one bag, every graph edge has both endpoints in at
30+
* least one common bag, and the reported width equals max(|bag|) - 1.
31+
* These three properties together with bag-connectedness are the
32+
* definition of a tree decomposition; CI breakage on any of them flags a
33+
* real algorithmic regression, not a cosmetic one.
34+
*/
35+
public class TreewidthAnalyzerTest {
36+
37+
private int edgeCounter;
38+
39+
@Before
40+
public void setUp() {
41+
edgeCounter = 0;
42+
}
43+
44+
// ─── Helpers ────────────────────────────────────────────────────
45+
46+
private Edge addEdge(Graph<String, Edge> g, String a, String b) {
47+
if (!g.containsVertex(a)) g.addVertex(a);
48+
if (!g.containsVertex(b)) g.addVertex(b);
49+
Edge e = new Edge("e" + (++edgeCounter), a, b);
50+
g.addEdge(e, a, b);
51+
return e;
52+
}
53+
54+
private Graph<String, Edge> path(int n) {
55+
Graph<String, Edge> g = new UndirectedSparseGraph<String, Edge>();
56+
for (int i = 0; i < n; i++) g.addVertex("v" + i);
57+
for (int i = 0; i < n - 1; i++) addEdge(g, "v" + i, "v" + (i + 1));
58+
return g;
59+
}
60+
61+
private Graph<String, Edge> cycle(int n) {
62+
Graph<String, Edge> g = new UndirectedSparseGraph<String, Edge>();
63+
for (int i = 0; i < n; i++) {
64+
addEdge(g, "v" + i, "v" + ((i + 1) % n));
65+
}
66+
return g;
67+
}
68+
69+
private Graph<String, Edge> complete(int n) {
70+
Graph<String, Edge> g = new UndirectedSparseGraph<String, Edge>();
71+
for (int i = 0; i < n; i++) g.addVertex("v" + i);
72+
for (int i = 0; i < n; i++) {
73+
for (int j = i + 1; j < n; j++) {
74+
addEdge(g, "v" + i, "v" + j);
75+
}
76+
}
77+
return g;
78+
}
79+
80+
/** Caterpillar tree: a path with one leaf hanging off each interior node. */
81+
private Graph<String, Edge> caterpillar(int spineLength) {
82+
Graph<String, Edge> g = path(spineLength);
83+
for (int i = 0; i < spineLength; i++) {
84+
addEdge(g, "v" + i, "leaf" + i);
85+
}
86+
return g;
87+
}
88+
89+
private Graph<String, Edge> grid(int rows, int cols) {
90+
Graph<String, Edge> g = new UndirectedSparseGraph<String, Edge>();
91+
for (int r = 0; r < rows; r++) {
92+
for (int c = 0; c < cols; c++) {
93+
g.addVertex(r + "," + c);
94+
}
95+
}
96+
for (int r = 0; r < rows; r++) {
97+
for (int c = 0; c < cols; c++) {
98+
if (c + 1 < cols) addEdge(g, r + "," + c, r + "," + (c + 1));
99+
if (r + 1 < rows) addEdge(g, r + "," + c, (r + 1) + "," + c);
100+
}
101+
}
102+
return g;
103+
}
104+
105+
/**
106+
* Verifies the two combinatorial invariants of a tree decomposition
107+
* that don't depend on the connectivity of the bag-tree:
108+
* vertex coverage and edge coverage. (Bag-connectedness — the third
109+
* defining property — is harder to assert without reconstructing the
110+
* tree, and is exercised indirectly by the elimination algorithm's
111+
* construction.)
112+
*/
113+
private void assertCoversGraph(Graph<String, Edge> g,
114+
TreewidthAnalyzer.TreewidthResult r) {
115+
TreewidthAnalyzer.TreeDecomposition d = r.getDecomposition();
116+
117+
// Vertex coverage
118+
Set<String> covered = new HashSet<String>();
119+
for (TreewidthAnalyzer.Bag bag : d.getBags()) {
120+
covered.addAll(bag.getVertices());
121+
}
122+
for (String v : g.getVertices()) {
123+
assertTrue("vertex " + v + " not covered by any bag",
124+
covered.contains(v));
125+
}
126+
127+
// Edge coverage: each graph edge has both endpoints in some shared bag
128+
for (Edge e : g.getEdges()) {
129+
edu.uci.ics.jung.graph.util.Pair<String> ends = g.getEndpoints(e);
130+
String a = ends.getFirst();
131+
String b = ends.getSecond();
132+
boolean found = false;
133+
for (TreewidthAnalyzer.Bag bag : d.getBags()) {
134+
if (bag.getVertices().contains(a) && bag.getVertices().contains(b)) {
135+
found = true;
136+
break;
137+
}
138+
}
139+
assertTrue("edge " + a + "--" + b + " not in any single bag", found);
140+
}
141+
142+
// Reported width matches max-bag-minus-one
143+
int maxBag = 0;
144+
for (TreewidthAnalyzer.Bag bag : d.getBags()) {
145+
maxBag = Math.max(maxBag, bag.getVertices().size());
146+
}
147+
if (d.getBags().isEmpty()) {
148+
assertEquals("empty decomposition has width 0", 0, d.getWidth());
149+
} else {
150+
assertEquals("reported width should equal max(|bag|)-1",
151+
maxBag - 1, d.getWidth());
152+
}
153+
}
154+
155+
// ─── Trivial cases ──────────────────────────────────────────────
156+
157+
@Test
158+
public void emptyGraphHasTreewidthZero() {
159+
TreewidthAnalyzer.TreewidthResult r =
160+
TreewidthAnalyzer.analyze(new UndirectedSparseGraph<String, Edge>());
161+
assertEquals(0, r.getLowerBound());
162+
assertEquals(0, r.getUpperBound());
163+
assertEquals(0, r.getVertices());
164+
assertEquals(0, r.getEdges());
165+
assertTrue(r.isExact());
166+
assertEquals(0, r.getDecomposition().getBags().size());
167+
assertEquals(0, r.getDecomposition().getTreeEdges().size());
168+
}
169+
170+
@Test
171+
public void singletonHasTreewidthZero() {
172+
Graph<String, Edge> g = new UndirectedSparseGraph<String, Edge>();
173+
g.addVertex("solo");
174+
TreewidthAnalyzer.TreewidthResult r = TreewidthAnalyzer.analyze(g);
175+
assertEquals(0, r.getUpperBound());
176+
assertEquals(0, r.getLowerBound());
177+
assertTrue(r.isExact());
178+
assertCoversGraph(g, r);
179+
}
180+
181+
@Test
182+
public void singleEdgeHasTreewidthOne() {
183+
Graph<String, Edge> g = new UndirectedSparseGraph<String, Edge>();
184+
addEdge(g, "a", "b");
185+
TreewidthAnalyzer.TreewidthResult r = TreewidthAnalyzer.analyze(g);
186+
assertEquals(1, r.getUpperBound());
187+
assertTrue("lower bound should be <= upper bound",
188+
r.getLowerBound() <= r.getUpperBound());
189+
assertCoversGraph(g, r);
190+
}
191+
192+
// ─── Forests / trees ────────────────────────────────────────────
193+
194+
@Test
195+
public void pathHasTreewidthOne() {
196+
Graph<String, Edge> g = path(7);
197+
TreewidthAnalyzer.TreewidthResult r = TreewidthAnalyzer.analyze(g);
198+
assertEquals("path P_7 should have treewidth 1", 1, r.getUpperBound());
199+
assertTrue(r.getLowerBound() <= 1);
200+
assertCoversGraph(g, r);
201+
}
202+
203+
@Test
204+
public void caterpillarHasTreewidthOne() {
205+
Graph<String, Edge> g = caterpillar(5);
206+
TreewidthAnalyzer.TreewidthResult r = TreewidthAnalyzer.analyze(g);
207+
assertEquals("trees have treewidth 1", 1, r.getUpperBound());
208+
assertCoversGraph(g, r);
209+
}
210+
211+
// ─── Cycles ─────────────────────────────────────────────────────
212+
213+
@Test
214+
public void triangleHasTreewidthTwo() {
215+
// K3 == C3
216+
Graph<String, Edge> g = complete(3);
217+
TreewidthAnalyzer.TreewidthResult r = TreewidthAnalyzer.analyze(g);
218+
assertEquals(2, r.getUpperBound());
219+
assertEquals(2, r.getLowerBound());
220+
assertTrue(r.isExact());
221+
assertCoversGraph(g, r);
222+
}
223+
224+
@Test
225+
public void cycleC5HasTreewidthTwo() {
226+
Graph<String, Edge> g = cycle(5);
227+
TreewidthAnalyzer.TreewidthResult r = TreewidthAnalyzer.analyze(g);
228+
assertEquals("cycle C_n (n>=3) has treewidth 2", 2, r.getUpperBound());
229+
assertCoversGraph(g, r);
230+
}
231+
232+
@Test
233+
public void cycleC8HasTreewidthTwo() {
234+
Graph<String, Edge> g = cycle(8);
235+
TreewidthAnalyzer.TreewidthResult r = TreewidthAnalyzer.analyze(g);
236+
assertEquals(2, r.getUpperBound());
237+
assertCoversGraph(g, r);
238+
}
239+
240+
// ─── Complete graphs ────────────────────────────────────────────
241+
242+
@Test
243+
public void k4HasTreewidthThree() {
244+
Graph<String, Edge> g = complete(4);
245+
TreewidthAnalyzer.TreewidthResult r = TreewidthAnalyzer.analyze(g);
246+
assertEquals(3, r.getUpperBound());
247+
assertEquals(3, r.getLowerBound());
248+
assertTrue("K_n bounds should be exact", r.isExact());
249+
assertCoversGraph(g, r);
250+
}
251+
252+
@Test
253+
public void k6HasTreewidthFive() {
254+
Graph<String, Edge> g = complete(6);
255+
TreewidthAnalyzer.TreewidthResult r = TreewidthAnalyzer.analyze(g);
256+
assertEquals(5, r.getUpperBound());
257+
assertEquals(5, r.getLowerBound());
258+
assertTrue(r.isExact());
259+
assertCoversGraph(g, r);
260+
}
261+
262+
// ─── Grids ──────────────────────────────────────────────────────
263+
264+
@Test
265+
public void smallGridBoundsBracketTrueTreewidth() {
266+
// 3x4 grid has true treewidth 3 (= min(3,4))
267+
Graph<String, Edge> g = grid(3, 4);
268+
TreewidthAnalyzer.TreewidthResult r = TreewidthAnalyzer.analyze(g);
269+
assertTrue("lower bound should be at least 2 for any 3-by-N grid",
270+
r.getLowerBound() >= 2);
271+
assertTrue("upper bound should not exceed n-1 = 11",
272+
r.getUpperBound() <= 11);
273+
assertTrue(r.getLowerBound() <= r.getUpperBound());
274+
assertCoversGraph(g, r);
275+
}
276+
277+
// ─── Disconnected graphs ────────────────────────────────────────
278+
279+
@Test
280+
public void independentSetHasTreewidthZero() {
281+
Graph<String, Edge> g = new UndirectedSparseGraph<String, Edge>();
282+
for (int i = 0; i < 5; i++) g.addVertex("v" + i);
283+
TreewidthAnalyzer.TreewidthResult r = TreewidthAnalyzer.analyze(g);
284+
assertEquals(0, r.getUpperBound());
285+
assertEquals(0, r.getLowerBound());
286+
assertCoversGraph(g, r);
287+
}
288+
289+
@Test
290+
public void twoDisconnectedTrianglesHasTreewidthTwo() {
291+
Graph<String, Edge> g = new UndirectedSparseGraph<String, Edge>();
292+
// triangle 1
293+
addEdge(g, "a", "b");
294+
addEdge(g, "b", "c");
295+
addEdge(g, "c", "a");
296+
// triangle 2
297+
addEdge(g, "x", "y");
298+
addEdge(g, "y", "z");
299+
addEdge(g, "z", "x");
300+
TreewidthAnalyzer.TreewidthResult r = TreewidthAnalyzer.analyze(g);
301+
assertEquals(2, r.getUpperBound());
302+
assertCoversGraph(g, r);
303+
}
304+
305+
// ─── Bounds ordering invariant ──────────────────────────────────
306+
307+
@Test
308+
public void lowerBoundNeverExceedsUpperBoundOnRandomDenseGraph() {
309+
// Build a moderately dense graph and check the bound ordering.
310+
// Uses a deterministic seed so the test is repeatable.
311+
java.util.Random rng = new java.util.Random(42L);
312+
int n = 12;
313+
Graph<String, Edge> g = new UndirectedSparseGraph<String, Edge>();
314+
for (int i = 0; i < n; i++) g.addVertex("v" + i);
315+
for (int i = 0; i < n; i++) {
316+
for (int j = i + 1; j < n; j++) {
317+
if (rng.nextDouble() < 0.4) addEdge(g, "v" + i, "v" + j);
318+
}
319+
}
320+
TreewidthAnalyzer.TreewidthResult r = TreewidthAnalyzer.analyze(g);
321+
assertTrue("lower <= upper", r.getLowerBound() <= r.getUpperBound());
322+
assertTrue("upper <= n-1", r.getUpperBound() <= n - 1);
323+
assertCoversGraph(g, r);
324+
}
325+
326+
// ─── Report ─────────────────────────────────────────────────────
327+
328+
@Test
329+
public void reportMentionsBoundsAndDecomposition() {
330+
String txt = TreewidthAnalyzer.report(complete(4));
331+
assertNotNull(txt);
332+
assertTrue("report should mention 'Treewidth'", txt.contains("Treewidth"));
333+
assertTrue("report should mention 'Bounds'", txt.contains("Bounds"));
334+
assertTrue("report should mention 'Tree Decomposition'",
335+
txt.contains("Tree Decomposition"));
336+
}
337+
338+
@Test
339+
public void reportOnEmptyGraphIsWellFormed() {
340+
String txt = TreewidthAnalyzer.report(new UndirectedSparseGraph<String, Edge>());
341+
assertNotNull(txt);
342+
assertTrue(txt.contains("Vertices: 0"));
343+
}
344+
345+
// ─── Bag / TreeEdge value semantics ─────────────────────────────
346+
347+
@Test
348+
public void bagWidthIsSizeMinusOne() {
349+
Set<String> vs = new HashSet<String>();
350+
vs.add("a"); vs.add("b"); vs.add("c");
351+
TreewidthAnalyzer.Bag bag = new TreewidthAnalyzer.Bag(7, vs);
352+
assertEquals(7, bag.getId());
353+
assertEquals(3, bag.getVertices().size());
354+
assertEquals(2, bag.width());
355+
assertTrue(bag.toString().contains("Bag 7"));
356+
}
357+
358+
@Test
359+
public void treeEdgeRetainsEndpoints() {
360+
TreewidthAnalyzer.TreeEdge e = new TreewidthAnalyzer.TreeEdge(3, 9);
361+
assertEquals(3, e.getBag1());
362+
assertEquals(9, e.getBag2());
363+
assertEquals("3 -- 9", e.toString());
364+
}
365+
366+
@Test
367+
public void bagVerticesAreUnmodifiable() {
368+
Set<String> vs = new HashSet<String>();
369+
vs.add("a");
370+
TreewidthAnalyzer.Bag bag = new TreewidthAnalyzer.Bag(0, vs);
371+
try {
372+
bag.getVertices().add("hacker");
373+
fail("Bag.getVertices() must return an unmodifiable view");
374+
} catch (UnsupportedOperationException expected) {
375+
// good
376+
}
377+
}
378+
}

0 commit comments

Comments
 (0)