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

Commit e7a2803

Browse files
feat: add Graph Complement Analyzer
Adds GraphComplementAnalyzer utility that computes the complement graph (all edges not in the original) and provides comparative analysis: - Edge count validation (orig + complement = complete graph) - Density and average degree comparison - Self-complementarity edge-count test - Top degree-change vertices between original and complement - Isolated vertex analysis (complement-isolated = universal in original) Usage: Call GraphComplementAnalyzer.analyze(graph) for a full report, or buildComplement(graph) to get the complement as a JUNG graph. Includes unit tests for all public methods.
1 parent 15ca172 commit e7a2803

2 files changed

Lines changed: 275 additions & 0 deletions

File tree

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
package gvisual;
2+
3+
import edu.uci.ics.jung.graph.Graph;
4+
import edu.uci.ics.jung.graph.UndirectedSparseGraph;
5+
import java.util.*;
6+
7+
/**
8+
* Computes the <b>complement graph</b> of a given graph and provides comparative
9+
* analysis between the original and its complement.
10+
*
11+
* <p>The complement G' of a graph G has the same vertices, but an edge exists in G'
12+
* if and only if it does <em>not</em> exist in G. This is useful for understanding
13+
* graph density, identifying missing connections, and studying structural properties
14+
* that become apparent when relationships are inverted.</p>
15+
*
16+
* <h3>Features</h3>
17+
* <ul>
18+
* <li>Build the complement graph as a new JUNG UndirectedSparseGraph</li>
19+
* <li>Compare edge counts, density, and degree distributions</li>
20+
* <li>Identify vertices whose degree changes most dramatically</li>
21+
* <li>Check self-complementarity (isomorphism with complement)</li>
22+
* <li>Export a textual comparison report</li>
23+
* </ul>
24+
*
25+
* @author zalenix
26+
*/
27+
public final class GraphComplementAnalyzer {
28+
29+
private GraphComplementAnalyzer() { /* utility class */ }
30+
31+
/**
32+
* Builds the complement of the given graph.
33+
*
34+
* @param graph the original graph
35+
* @return a new graph containing all edges not present in the original
36+
*/
37+
public static Graph<String, edge> buildComplement(Graph<String, edge> graph) {
38+
UndirectedSparseGraph<String, edge> complement = new UndirectedSparseGraph<>();
39+
List<String> vertices = new ArrayList<>(graph.getVertices());
40+
41+
for (String v : vertices) {
42+
complement.addVertex(v);
43+
}
44+
45+
Set<String> existingEdges = new HashSet<>();
46+
for (edge e : graph.getEdges()) {
47+
String v1 = e.getVertex1();
48+
String v2 = e.getVertex2();
49+
existingEdges.add(edgeKey(v1, v2));
50+
}
51+
52+
int edgeId = 0;
53+
for (int i = 0; i < vertices.size(); i++) {
54+
for (int j = i + 1; j < vertices.size(); j++) {
55+
String v1 = vertices.get(i);
56+
String v2 = vertices.get(j);
57+
if (!existingEdges.contains(edgeKey(v1, v2))) {
58+
edge e = new edge(v1, v2, "complement_" + edgeId++);
59+
complement.addEdge(e, v1, v2);
60+
}
61+
}
62+
}
63+
64+
return complement;
65+
}
66+
67+
/**
68+
* Generates a comparative analysis report between the original graph
69+
* and its complement.
70+
*
71+
* @param graph the original graph
72+
* @return a formatted analysis report string
73+
*/
74+
public static String analyze(Graph<String, edge> graph) {
75+
Graph<String, edge> complement = buildComplement(graph);
76+
int n = graph.getVertexCount();
77+
int origEdges = graph.getEdgeCount();
78+
int compEdges = complement.getEdgeCount();
79+
int maxEdges = n * (n - 1) / 2;
80+
81+
StringBuilder sb = new StringBuilder();
82+
sb.append("═══════════════════════════════════════════\n");
83+
sb.append(" GRAPH COMPLEMENT ANALYSIS\n");
84+
sb.append("═══════════════════════════════════════════\n\n");
85+
86+
sb.append("Vertices: ").append(n).append("\n");
87+
sb.append("Max possible edges: ").append(maxEdges).append("\n\n");
88+
89+
sb.append("── Original Graph ─────────────────────────\n");
90+
sb.append(" Edges: ").append(origEdges).append("\n");
91+
sb.append(String.format(" Density: %.4f%n", density(origEdges, n)));
92+
sb.append(String.format(" Avg degree: %.2f%n", avgDegree(graph)));
93+
sb.append("\n");
94+
95+
sb.append("── Complement Graph ───────────────────────\n");
96+
sb.append(" Edges: ").append(compEdges).append("\n");
97+
sb.append(String.format(" Density: %.4f%n", density(compEdges, n)));
98+
sb.append(String.format(" Avg degree: %.2f%n", avgDegree(complement)));
99+
sb.append("\n");
100+
101+
// Verify edge counts sum correctly
102+
sb.append("── Validation ─────────────────────────────\n");
103+
sb.append(" Orig + Complement: ").append(origEdges + compEdges).append("\n");
104+
sb.append(" Expected (n*(n-1)/2):").append(maxEdges).append("\n");
105+
sb.append(" Valid: ").append(origEdges + compEdges == maxEdges ? "✓" : "✗").append("\n\n");
106+
107+
// Self-complementary check (quick heuristic: edge count must equal n*(n-1)/4)
108+
boolean couldBeSelfComplementary = (maxEdges % 2 == 0) && (origEdges == maxEdges / 2);
109+
sb.append("── Self-Complementary ─────────────────────\n");
110+
sb.append(" Edge-count test: ").append(couldBeSelfComplementary ? "PASS (possible)" : "FAIL").append("\n");
111+
if (couldBeSelfComplementary) {
112+
sb.append(" (Full isomorphism check not performed — edge count is necessary but not sufficient)\n");
113+
}
114+
sb.append("\n");
115+
116+
// Top degree changes
117+
sb.append("── Largest Degree Changes ─────────────────\n");
118+
List<DegreeChange> changes = computeDegreeChanges(graph, complement);
119+
changes.sort((a, b) -> Integer.compare(b.absDelta, a.absDelta));
120+
int show = Math.min(10, changes.size());
121+
sb.append(String.format(" %-20s %8s %8s %8s%n", "Vertex", "Original", "Compl.", "Delta"));
122+
sb.append(" ").append("-".repeat(48)).append("\n");
123+
for (int i = 0; i < show; i++) {
124+
DegreeChange dc = changes.get(i);
125+
sb.append(String.format(" %-20s %8d %8d %+8d%n",
126+
truncate(dc.vertex, 20), dc.origDeg, dc.compDeg, dc.compDeg - dc.origDeg));
127+
}
128+
sb.append("\n");
129+
130+
// Isolated vertices analysis
131+
long origIsolated = graph.getVertices().stream().filter(v -> graph.degree(v) == 0).count();
132+
long compIsolated = complement.getVertices().stream().filter(v -> complement.degree(v) == 0).count();
133+
sb.append("── Isolated Vertices ──────────────────────\n");
134+
sb.append(" In original: ").append(origIsolated).append("\n");
135+
sb.append(" In complement: ").append(compIsolated).append("\n");
136+
sb.append(" (Isolated in complement = universal vertices in original)\n");
137+
138+
return sb.toString();
139+
}
140+
141+
/**
142+
* Returns the complement graph's edge list as a list of string pairs.
143+
*
144+
* @param graph the original graph
145+
* @return list of [vertex1, vertex2] arrays representing complement edges
146+
*/
147+
public static List<String[]> getComplementEdgeList(Graph<String, edge> graph) {
148+
Graph<String, edge> complement = buildComplement(graph);
149+
List<String[]> result = new ArrayList<>();
150+
for (edge e : complement.getEdges()) {
151+
result.add(new String[]{e.getVertex1(), e.getVertex2()});
152+
}
153+
return result;
154+
}
155+
156+
// ── Internal helpers ──────────────────────────────────────
157+
158+
private static String edgeKey(String v1, String v2) {
159+
return v1.compareTo(v2) < 0 ? v1 + "|" + v2 : v2 + "|" + v1;
160+
}
161+
162+
private static double density(int edges, int vertices) {
163+
if (vertices < 2) return 0.0;
164+
return (2.0 * edges) / (vertices * (vertices - 1));
165+
}
166+
167+
private static double avgDegree(Graph<String, edge> g) {
168+
if (g.getVertexCount() == 0) return 0.0;
169+
double sum = 0;
170+
for (String v : g.getVertices()) {
171+
sum += g.degree(v);
172+
}
173+
return sum / g.getVertexCount();
174+
}
175+
176+
private static List<DegreeChange> computeDegreeChanges(
177+
Graph<String, edge> orig, Graph<String, edge> comp) {
178+
List<DegreeChange> list = new ArrayList<>();
179+
for (String v : orig.getVertices()) {
180+
int od = orig.degree(v);
181+
int cd = comp.degree(v);
182+
list.add(new DegreeChange(v, od, cd));
183+
}
184+
return list;
185+
}
186+
187+
private static String truncate(String s, int max) {
188+
return s.length() <= max ? s : s.substring(0, max - 1) + "…";
189+
}
190+
191+
private static final class DegreeChange {
192+
final String vertex;
193+
final int origDeg;
194+
final int compDeg;
195+
final int absDelta;
196+
197+
DegreeChange(String vertex, int origDeg, int compDeg) {
198+
this.vertex = vertex;
199+
this.origDeg = origDeg;
200+
this.compDeg = compDeg;
201+
this.absDelta = Math.abs(compDeg - origDeg);
202+
}
203+
}
204+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package gvisual;
2+
3+
import edu.uci.ics.jung.graph.Graph;
4+
import edu.uci.ics.jung.graph.UndirectedSparseGraph;
5+
import org.junit.Test;
6+
import static org.junit.Assert.*;
7+
8+
import java.util.List;
9+
10+
/**
11+
* Tests for {@link GraphComplementAnalyzer}.
12+
*/
13+
public class GraphComplementAnalyzerTest {
14+
15+
private Graph<String, edge> makeTriangle() {
16+
UndirectedSparseGraph<String, edge> g = new UndirectedSparseGraph<>();
17+
g.addVertex("A"); g.addVertex("B"); g.addVertex("C");
18+
edge e1 = new edge("A", "B", "e1");
19+
edge e2 = new edge("B", "C", "e2");
20+
g.addEdge(e1, "A", "B");
21+
g.addEdge(e2, "B", "C");
22+
return g;
23+
}
24+
25+
@Test
26+
public void complementEdgeCountPlusOriginalEqualsComplete() {
27+
Graph<String, edge> g = makeTriangle();
28+
Graph<String, edge> comp = GraphComplementAnalyzer.buildComplement(g);
29+
int n = g.getVertexCount();
30+
int maxEdges = n * (n - 1) / 2;
31+
assertEquals(maxEdges, g.getEdgeCount() + comp.getEdgeCount());
32+
}
33+
34+
@Test
35+
public void complementOfCompleteGraphIsEmpty() {
36+
UndirectedSparseGraph<String, edge> g = new UndirectedSparseGraph<>();
37+
g.addVertex("A"); g.addVertex("B"); g.addVertex("C");
38+
g.addEdge(new edge("A", "B", "1"), "A", "B");
39+
g.addEdge(new edge("B", "C", "2"), "B", "C");
40+
g.addEdge(new edge("A", "C", "3"), "A", "C");
41+
42+
Graph<String, edge> comp = GraphComplementAnalyzer.buildComplement(g);
43+
assertEquals(0, comp.getEdgeCount());
44+
}
45+
46+
@Test
47+
public void complementPreservesVertices() {
48+
Graph<String, edge> g = makeTriangle();
49+
Graph<String, edge> comp = GraphComplementAnalyzer.buildComplement(g);
50+
assertEquals(g.getVertexCount(), comp.getVertexCount());
51+
for (String v : g.getVertices()) {
52+
assertTrue(comp.containsVertex(v));
53+
}
54+
}
55+
56+
@Test
57+
public void analyzeProducesReport() {
58+
Graph<String, edge> g = makeTriangle();
59+
String report = GraphComplementAnalyzer.analyze(g);
60+
assertTrue(report.contains("GRAPH COMPLEMENT ANALYSIS"));
61+
assertTrue(report.contains("Density"));
62+
}
63+
64+
@Test
65+
public void getComplementEdgeListWorks() {
66+
Graph<String, edge> g = makeTriangle();
67+
List<String[]> edges = GraphComplementAnalyzer.getComplementEdgeList(g);
68+
// Triangle with 2 edges, 3 vertices → max 3 edges → complement has 1 edge
69+
assertEquals(1, edges.size());
70+
}
71+
}

0 commit comments

Comments
 (0)