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

Commit e92bbc2

Browse files
author
Repo Gardener
committed
test(GraphDegreeSequenceRandomizer): add 16 unit tests covering edge-switching invariants
Verifies the core null-model contract: every randomization preserves the per-vertex degree map, vertex/edge counts, and the simple-graph property (no self-loops, no multi-edges). Also exercises ensemble generation, seed reproducibility, edge cases (empty graph, single edge, isolated vertices, swapFactor<=0), and the NullModelSummary stats (mean, std, z-score, p-value, isSignificant, toString). All tests pass against the existing implementation.
1 parent 01b91ce commit e92bbc2

1 file changed

Lines changed: 322 additions & 0 deletions

File tree

Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
package gvisual;
2+
3+
import edu.uci.ics.jung.graph.Graph;
4+
import edu.uci.ics.jung.graph.UndirectedSparseGraph;
5+
import gvisual.GraphDegreeSequenceRandomizer.NullModelSummary;
6+
import org.junit.Before;
7+
import org.junit.Test;
8+
9+
import java.util.*;
10+
11+
import static org.junit.Assert.*;
12+
13+
/**
14+
* Unit tests for {@link GraphDegreeSequenceRandomizer}.
15+
*
16+
* <p>Covers:</p>
17+
* <ul>
18+
* <li>Degree sequence preservation (the core invariant of edge-switching).</li>
19+
* <li>Vertex and edge count preservation.</li>
20+
* <li>Absence of self-loops and multi-edges in randomized output.</li>
21+
* <li>Reproducibility with a fixed seed.</li>
22+
* <li>Ensemble generation.</li>
23+
* <li>Edge cases: empty graph, single edge, isolated vertices.</li>
24+
* <li>Significance computation (mean / std / z / p / isSignificant).</li>
25+
* <li>NullModelSummary getters + toString.</li>
26+
* </ul>
27+
*/
28+
public class GraphDegreeSequenceRandomizerTest {
29+
30+
private Graph<String, Edge> graph;
31+
32+
@Before
33+
public void setUp() {
34+
graph = new UndirectedSparseGraph<String, Edge>();
35+
}
36+
37+
// ---- helpers -------------------------------------------------------
38+
39+
private void addEdge(String a, String b) {
40+
if (!graph.containsVertex(a)) graph.addVertex(a);
41+
if (!graph.containsVertex(b)) graph.addVertex(b);
42+
Edge e = new Edge("f", a, b);
43+
e.setWeight(1.0f);
44+
graph.addEdge(e, a, b);
45+
}
46+
47+
private static Map<String, Integer> degreeMap(Graph<String, Edge> g) {
48+
Map<String, Integer> deg = new TreeMap<String, Integer>();
49+
for (String v : g.getVertices()) {
50+
deg.put(v, g.getNeighborCount(v));
51+
}
52+
return deg;
53+
}
54+
55+
private static List<Integer> degreeSequence(Graph<String, Edge> g) {
56+
List<Integer> degs = new ArrayList<Integer>();
57+
for (String v : g.getVertices()) {
58+
degs.add(g.getNeighborCount(v));
59+
}
60+
Collections.sort(degs);
61+
return degs;
62+
}
63+
64+
// ---- core invariants ----------------------------------------------
65+
66+
@Test
67+
public void testDegreeSequencePreservedOnTriangle() {
68+
addEdge("a", "b");
69+
addEdge("b", "c");
70+
addEdge("a", "c");
71+
72+
GraphDegreeSequenceRandomizer rand = new GraphDegreeSequenceRandomizer(42L);
73+
Graph<String, Edge> shuffled = rand.randomize(graph, 10);
74+
75+
assertEquals(degreeSequence(graph), degreeSequence(shuffled));
76+
}
77+
78+
@Test
79+
public void testPerVertexDegreePreservedOnPath() {
80+
addEdge("a", "b");
81+
addEdge("b", "c");
82+
addEdge("c", "d");
83+
addEdge("d", "e");
84+
85+
GraphDegreeSequenceRandomizer rand = new GraphDegreeSequenceRandomizer(1234L);
86+
Graph<String, Edge> shuffled = rand.randomize(graph, 20);
87+
88+
assertEquals("per-vertex degree map must be preserved",
89+
degreeMap(graph), degreeMap(shuffled));
90+
}
91+
92+
@Test
93+
public void testVertexAndEdgeCountsPreserved() {
94+
// Build a denser graph.
95+
String[] vs = {"a", "b", "c", "d", "e"};
96+
addEdge("a", "b");
97+
addEdge("a", "c");
98+
addEdge("a", "d");
99+
addEdge("b", "c");
100+
addEdge("b", "e");
101+
addEdge("c", "d");
102+
103+
GraphDegreeSequenceRandomizer rand = new GraphDegreeSequenceRandomizer(99L);
104+
Graph<String, Edge> shuffled = rand.randomize(graph, 25);
105+
106+
assertEquals(graph.getVertexCount(), shuffled.getVertexCount());
107+
assertEquals(graph.getEdgeCount(), shuffled.getEdgeCount());
108+
// Vertex set should be the same.
109+
assertEquals(new TreeSet<String>(graph.getVertices()),
110+
new TreeSet<String>(shuffled.getVertices()));
111+
112+
// Use the helper variable to assert all original vertices survive.
113+
for (String v : vs) {
114+
assertTrue("vertex " + v + " missing", shuffled.containsVertex(v));
115+
}
116+
}
117+
118+
@Test
119+
public void testNoSelfLoopsOrMultiEdges() {
120+
// K_5 minus one edge -> some swap targets are blocked by existing edges.
121+
String[] vs = {"a", "b", "c", "d", "e"};
122+
for (int i = 0; i < vs.length; i++) {
123+
for (int j = i + 1; j < vs.length; j++) {
124+
if (i == 0 && j == 4) continue; // remove a-e
125+
addEdge(vs[i], vs[j]);
126+
}
127+
}
128+
129+
GraphDegreeSequenceRandomizer rand = new GraphDegreeSequenceRandomizer(7L);
130+
Graph<String, Edge> shuffled = rand.randomize(graph, 30);
131+
132+
// Verify no self-loops.
133+
for (Edge e : shuffled.getEdges()) {
134+
String u = shuffled.getEndpoints(e).getFirst();
135+
String v = shuffled.getEndpoints(e).getSecond();
136+
assertNotEquals("self-loop created: " + u, u, v);
137+
}
138+
139+
// Verify no parallel edges (since UndirectedSparseGraph already
140+
// forbids them, this is a structural sanity check on edge count).
141+
Set<String> seenPairs = new HashSet<String>();
142+
for (Edge e : shuffled.getEdges()) {
143+
String u = shuffled.getEndpoints(e).getFirst();
144+
String v = shuffled.getEndpoints(e).getSecond();
145+
String key = u.compareTo(v) < 0 ? u + "|" + v : v + "|" + u;
146+
assertTrue("duplicate edge between " + key, seenPairs.add(key));
147+
}
148+
}
149+
150+
@Test
151+
public void testReturnsCopyNotOriginal() {
152+
addEdge("a", "b");
153+
addEdge("b", "c");
154+
addEdge("a", "c");
155+
156+
GraphDegreeSequenceRandomizer rand = new GraphDegreeSequenceRandomizer(1L);
157+
Graph<String, Edge> shuffled = rand.randomize(graph, 5);
158+
159+
assertNotSame("randomize must produce a new graph instance",
160+
graph, shuffled);
161+
162+
// Mutating the copy must not affect the original.
163+
int origEdgeCount = graph.getEdgeCount();
164+
for (Edge e : new ArrayList<Edge>(shuffled.getEdges())) {
165+
shuffled.removeEdge(e);
166+
}
167+
assertEquals(origEdgeCount, graph.getEdgeCount());
168+
}
169+
170+
// ---- reproducibility ----------------------------------------------
171+
172+
@Test
173+
public void testSameSeedProducesSameDegreeSequence() {
174+
// Same seed -> same random walk -> same final degree sequence.
175+
// (Cannot guarantee identical edge sets because graph-internal
176+
// ordering may differ, but the degree map per vertex must match.)
177+
addEdge("a", "b");
178+
addEdge("b", "c");
179+
addEdge("c", "d");
180+
addEdge("d", "e");
181+
addEdge("a", "e");
182+
183+
GraphDegreeSequenceRandomizer r1 = new GraphDegreeSequenceRandomizer(123L);
184+
GraphDegreeSequenceRandomizer r2 = new GraphDegreeSequenceRandomizer(123L);
185+
Graph<String, Edge> g1 = r1.randomize(graph, 5);
186+
Graph<String, Edge> g2 = r2.randomize(graph, 5);
187+
188+
assertEquals(degreeMap(g1), degreeMap(g2));
189+
}
190+
191+
// ---- ensemble -----------------------------------------------------
192+
193+
@Test
194+
public void testEnsembleProducesRequestedCount() {
195+
addEdge("a", "b");
196+
addEdge("b", "c");
197+
addEdge("c", "d");
198+
addEdge("a", "d");
199+
200+
GraphDegreeSequenceRandomizer rand = new GraphDegreeSequenceRandomizer(42L);
201+
List<Graph<String, Edge>> ensemble = rand.ensemble(graph, 5, 4);
202+
203+
assertEquals(5, ensemble.size());
204+
List<Integer> origSeq = degreeSequence(graph);
205+
for (Graph<String, Edge> g : ensemble) {
206+
assertEquals("each ensemble graph must preserve the degree sequence",
207+
origSeq, degreeSequence(g));
208+
assertEquals(graph.getVertexCount(), g.getVertexCount());
209+
assertEquals(graph.getEdgeCount(), g.getEdgeCount());
210+
}
211+
}
212+
213+
@Test
214+
public void testEnsembleZeroCount() {
215+
addEdge("a", "b");
216+
GraphDegreeSequenceRandomizer rand = new GraphDegreeSequenceRandomizer(0L);
217+
List<Graph<String, Edge>> ensemble = rand.ensemble(graph, 0, 5);
218+
assertNotNull(ensemble);
219+
assertEquals(0, ensemble.size());
220+
}
221+
222+
// ---- edge cases ---------------------------------------------------
223+
224+
@Test
225+
public void testEmptyGraphRandomizes() {
226+
GraphDegreeSequenceRandomizer rand = new GraphDegreeSequenceRandomizer(0L);
227+
Graph<String, Edge> shuffled = rand.randomize(graph, 10);
228+
assertEquals(0, shuffled.getVertexCount());
229+
assertEquals(0, shuffled.getEdgeCount());
230+
}
231+
232+
@Test
233+
public void testSingleEdgeGraphUnchanged() {
234+
addEdge("a", "b");
235+
GraphDegreeSequenceRandomizer rand = new GraphDegreeSequenceRandomizer(0L);
236+
Graph<String, Edge> shuffled = rand.randomize(graph, 100);
237+
assertEquals(2, shuffled.getVertexCount());
238+
assertEquals(1, shuffled.getEdgeCount());
239+
assertEquals(degreeMap(graph), degreeMap(shuffled));
240+
}
241+
242+
@Test
243+
public void testIsolatedVerticesPreserved() {
244+
addEdge("a", "b");
245+
graph.addVertex("c"); // isolated
246+
graph.addVertex("d"); // isolated
247+
GraphDegreeSequenceRandomizer rand = new GraphDegreeSequenceRandomizer(99L);
248+
Graph<String, Edge> shuffled = rand.randomize(graph, 5);
249+
assertTrue(shuffled.containsVertex("c"));
250+
assertTrue(shuffled.containsVertex("d"));
251+
assertEquals(0, shuffled.getNeighborCount("c"));
252+
assertEquals(0, shuffled.getNeighborCount("d"));
253+
}
254+
255+
@Test
256+
public void testSwapFactorZeroOrNegativeStillRuns() {
257+
addEdge("a", "b");
258+
addEdge("b", "c");
259+
addEdge("a", "c");
260+
GraphDegreeSequenceRandomizer rand = new GraphDegreeSequenceRandomizer(1L);
261+
// swapFactor <= 0 is clamped to 1 internally; must not throw.
262+
Graph<String, Edge> g1 = rand.randomize(graph, 0);
263+
Graph<String, Edge> g2 = rand.randomize(graph, -5);
264+
assertEquals(degreeMap(graph), degreeMap(g1));
265+
assertEquals(degreeMap(graph), degreeMap(g2));
266+
}
267+
268+
// ---- significance computation -------------------------------------
269+
270+
@Test
271+
public void testComputeSignificanceBasicStats() {
272+
GraphDegreeSequenceRandomizer rand = new GraphDegreeSequenceRandomizer();
273+
double[] ensemble = {1.0, 2.0, 3.0, 4.0, 5.0};
274+
NullModelSummary s = rand.computeSignificance(2.5, ensemble);
275+
276+
// Mean = 3.0, population variance = 2.0, std = sqrt(2) ≈ 1.4142
277+
assertEquals(2.5, s.getObservedValue(), 1e-9);
278+
assertEquals(3.0, s.getEnsembleMean(), 1e-9);
279+
assertEquals(Math.sqrt(2.0), s.getEnsembleStdDev(), 1e-9);
280+
assertEquals((2.5 - 3.0) / Math.sqrt(2.0), s.getZScore(), 1e-9);
281+
// p = fraction of ensemble values >= 2.5 = 3/5 = 0.6 (3, 4, 5 qualify)
282+
assertEquals(0.6, s.getPValue(), 1e-9);
283+
assertEquals(5, s.getEnsembleSize());
284+
}
285+
286+
@Test
287+
public void testComputeSignificanceZeroStdDevGivesZeroZ() {
288+
GraphDegreeSequenceRandomizer rand = new GraphDegreeSequenceRandomizer();
289+
double[] ensemble = {2.0, 2.0, 2.0, 2.0};
290+
NullModelSummary s = rand.computeSignificance(5.0, ensemble);
291+
assertEquals(0.0, s.getEnsembleStdDev(), 1e-9);
292+
assertEquals(0.0, s.getZScore(), 1e-9);
293+
// p = 0/4 = 0 (all ensemble values < observed)
294+
assertEquals(0.0, s.getPValue(), 1e-9);
295+
}
296+
297+
@Test
298+
public void testIsSignificantThresholds() {
299+
GraphDegreeSequenceRandomizer rand = new GraphDegreeSequenceRandomizer();
300+
NullModelSummary low = rand.computeSignificance(
301+
10.0, new double[]{1.0, 2.0, 3.0, 4.0}); // p = 0
302+
assertTrue("p=0 is significant at alpha=0.05", low.isSignificant(0.05));
303+
304+
NullModelSummary high = rand.computeSignificance(
305+
0.0, new double[]{1.0, 2.0, 3.0, 4.0}); // p = 1.0
306+
assertFalse("p=1 is not significant at alpha=0.05", high.isSignificant(0.05));
307+
}
308+
309+
@Test
310+
public void testNullModelSummaryToStringContainsKeyFields() {
311+
GraphDegreeSequenceRandomizer rand = new GraphDegreeSequenceRandomizer();
312+
NullModelSummary s = rand.computeSignificance(
313+
2.0, new double[]{1.0, 2.0, 3.0});
314+
String txt = s.toString();
315+
assertTrue(txt.contains("observed"));
316+
assertTrue(txt.contains("mean"));
317+
assertTrue(txt.contains("std"));
318+
assertTrue(txt.contains("z"));
319+
assertTrue(txt.contains("p"));
320+
assertTrue(txt.contains("n="));
321+
}
322+
}

0 commit comments

Comments
 (0)