|
#!/usr/bin/python3.12 |
|
""" |
|
Graph Automorphisms: Complete Explanation with Examples |
|
|
|
An AUTOMORPHISM is an isomorphism from a graph to itself. |
|
It's a way to relabel vertices that preserves all edges. |
|
|
|
Think of it as "symmetries" of the graph. |
|
""" |
|
|
|
import networkx as nx |
|
import itertools |
|
from collections import defaultdict |
|
import matplotlib.pyplot as plt |
|
import os |
|
|
|
|
|
# Create output directory for images |
|
OUTPUT_DIR = "automorphism_illustrations" |
|
if not os.path.exists(OUTPUT_DIR): |
|
os.makedirs(OUTPUT_DIR) |
|
|
|
|
|
def save_graph_visualization(G, filename, title, pos=None, highlight_nodes=None, node_labels=None): |
|
""" |
|
Save a graph visualization as PNG. |
|
|
|
Args: |
|
G: NetworkX graph |
|
filename: Output filename (without extension) |
|
title: Plot title |
|
pos: Node positions (if None, spring layout is used) |
|
highlight_nodes: Dict of {node: color} to highlight specific nodes |
|
node_labels: Custom node labels (if None, uses node names) |
|
""" |
|
plt.figure(figsize=(10, 8)) |
|
|
|
# Calculate layout if not provided |
|
if pos is None: |
|
pos = nx.spring_layout(G, seed=42) |
|
|
|
# Default node colors |
|
node_colors = ['lightblue'] * len(G.nodes()) |
|
if highlight_nodes: |
|
node_list = list(G.nodes()) |
|
for i, node in enumerate(node_list): |
|
if node in highlight_nodes: |
|
node_colors[i] = highlight_nodes[node] |
|
|
|
# Draw graph |
|
nx.draw_networkx_edges(G, pos, width=2, alpha=0.6) |
|
nx.draw_networkx_nodes(G, pos, node_color=node_colors, |
|
node_size=1500, alpha=0.9, edgecolors='black', linewidths=2) |
|
|
|
# Labels |
|
labels = node_labels if node_labels else {node: str(node) for node in G.nodes()} |
|
nx.draw_networkx_labels(G, pos, labels, font_size=16, font_weight='bold') |
|
|
|
plt.title(title, fontsize=18, fontweight='bold', pad=20) |
|
plt.axis('off') |
|
plt.tight_layout() |
|
|
|
# Save |
|
filepath = os.path.join(OUTPUT_DIR, f"{filename}.png") |
|
plt.savefig(filepath, dpi=150, bbox_inches='tight', facecolor='white') |
|
plt.close() |
|
|
|
return filepath |
|
|
|
|
|
def visualize_automorphism(G, original_pos, automorphism, filename, title): |
|
""" |
|
Visualize an automorphism by showing the mapping. |
|
""" |
|
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 7)) |
|
|
|
# Left: Original graph |
|
plt.sca(ax1) |
|
nx.draw_networkx_edges(G, original_pos, width=2, alpha=0.6) |
|
nx.draw_networkx_nodes(G, original_pos, node_color='lightblue', |
|
node_size=1500, alpha=0.9, edgecolors='black', linewidths=2) |
|
labels = {node: str(node) for node in G.nodes()} |
|
nx.draw_networkx_labels(G, original_pos, labels, font_size=16, font_weight='bold') |
|
ax1.set_title("Original", fontsize=16, fontweight='bold') |
|
ax1.axis('off') |
|
|
|
# Right: After automorphism |
|
plt.sca(ax2) |
|
|
|
# Create new positions based on automorphism |
|
new_pos = {automorphism[node]: original_pos[node] for node in G.nodes()} |
|
|
|
nx.draw_networkx_edges(G, new_pos, width=2, alpha=0.6) |
|
nx.draw_networkx_nodes(G, new_pos, node_color='lightgreen', |
|
node_size=1500, alpha=0.9, edgecolors='black', linewidths=2) |
|
new_labels = {node: str(node) for node in G.nodes()} |
|
nx.draw_networkx_labels(G, new_pos, new_labels, font_size=16, font_weight='bold') |
|
ax2.set_title("After Automorphism", fontsize=16, fontweight='bold') |
|
ax2.axis('off') |
|
|
|
# Add mapping text |
|
mapping_text = " → ".join([f"{k}→{v}" for k, v in list(automorphism.items())[:4]]) |
|
if len(automorphism) > 4: |
|
mapping_text += "..." |
|
fig.suptitle(f"{title}\nMapping: {mapping_text}", fontsize=14, fontweight='bold') |
|
|
|
plt.tight_layout() |
|
filepath = os.path.join(OUTPUT_DIR, f"{filename}.png") |
|
plt.savefig(filepath, dpi=150, bbox_inches='tight', facecolor='white') |
|
plt.close() |
|
|
|
return filepath |
|
|
|
|
|
class AutomorphismAnalyzer: |
|
""" |
|
Analyze and find automorphisms of graphs. |
|
""" |
|
|
|
def find_all_automorphisms_bruteforce(self, G): |
|
""" |
|
Find all automorphisms by trying all vertex permutations. |
|
Only works for small graphs (< 10 nodes). |
|
|
|
An automorphism is a permutation of vertices that preserves edges. |
|
""" |
|
vertices = list(G.nodes()) |
|
n = len(vertices) |
|
automorphisms = [] |
|
|
|
# Try all n! permutations |
|
for perm in itertools.permutations(vertices): |
|
# Create mapping: old vertex -> new vertex |
|
mapping = {vertices[i]: perm[i] for i in range(n)} |
|
|
|
# Check if this mapping preserves all edges |
|
is_automorphism = True |
|
|
|
# Check that all existing edges are preserved |
|
for u, v in G.edges(): |
|
# After relabeling, edge (u,v) becomes (mapping[u], mapping[v]) |
|
# This should still be an edge in G |
|
if not G.has_edge(mapping[u], mapping[v]): |
|
is_automorphism = False |
|
break |
|
|
|
if is_automorphism: |
|
# Also verify that no extra edges are created |
|
# Count edges: should be same before and after mapping |
|
edge_count = 0 |
|
for u, v in G.edges(): |
|
if G.has_edge(mapping[u], mapping[v]): |
|
edge_count += 1 |
|
|
|
# If edge count matches, this is a valid automorphism |
|
if edge_count == G.number_of_edges(): |
|
automorphisms.append(mapping) |
|
|
|
return automorphisms |
|
|
|
def count_automorphisms(self, G): |
|
"""Count number of automorphisms (automorphism group size).""" |
|
autos = self.find_all_automorphisms_bruteforce(G) |
|
return len(autos) |
|
|
|
def describe_automorphisms(self, G): |
|
""" |
|
Provide human-readable description of automorphisms. |
|
""" |
|
automorphisms = self.find_all_automorphisms_bruteforce(G) |
|
|
|
result = { |
|
'count': len(automorphisms), |
|
'automorphisms': automorphisms, |
|
'is_symmetric': len(automorphisms) > 1, |
|
'symmetry_description': self._describe_symmetry(G, automorphisms) |
|
} |
|
|
|
return result |
|
|
|
def _describe_symmetry(self, G, automorphisms): |
|
"""Describe the type of symmetry.""" |
|
n = len(automorphisms) |
|
|
|
if n == 1: |
|
return "Asymmetric (no non-trivial automorphisms)" |
|
elif n == 2: |
|
return "One non-trivial symmetry (e.g., reflection)" |
|
else: |
|
return f"Highly symmetric ({n} automorphisms = {n}! / |Aut(G)|)" |
|
|
|
def find_orbits(self, G): |
|
""" |
|
Find orbits: sets of vertices that can be mapped to each other. |
|
Vertices in the same orbit are "equivalent" under symmetry. |
|
""" |
|
automorphisms = self.find_all_automorphisms_bruteforce(G) |
|
vertices = list(G.nodes()) |
|
|
|
# Union-find to group vertices that can be mapped to each other |
|
parent = {v: v for v in vertices} |
|
|
|
def find(v): |
|
# Path compression: find root and update all nodes on path |
|
root = v |
|
while parent[root] != root: |
|
root = parent[root] |
|
|
|
# Compress path |
|
while parent[v] != root: |
|
next_v = parent[v] |
|
parent[v] = root |
|
v = next_v |
|
|
|
return root |
|
|
|
def union(u, v): |
|
root_u = find(u) |
|
root_v = find(v) |
|
if root_u != root_v: |
|
parent[root_u] = root_v |
|
|
|
# For each automorphism, union vertices that map to each other |
|
for auto in automorphisms: |
|
for v in vertices: |
|
# If automorphism maps v to auto[v], they're in same orbit |
|
if v != auto[v]: # Only union if different |
|
union(v, auto[v]) |
|
|
|
# Group vertices by their root (orbit representative) |
|
orbits_dict = defaultdict(list) |
|
for v in vertices: |
|
root = find(v) |
|
orbits_dict[root].append(v) |
|
|
|
return list(orbits_dict.values()) |
|
|
|
|
|
def example_1_triangle(): |
|
""" |
|
Example 1: Equilateral Triangle |
|
High symmetry - all vertices are equivalent |
|
""" |
|
print("="*70) |
|
print("EXAMPLE 1: Equilateral Triangle") |
|
print("="*70) |
|
print("\nGraph structure:") |
|
print(" A") |
|
print(" / \\") |
|
print(" B---C") |
|
print("\nAll vertices look the same - complete symmetry!") |
|
|
|
G = nx.Graph() |
|
G.add_edges_from([('A', 'B'), ('B', 'C'), ('C', 'A')]) |
|
|
|
# Create visualization with fixed positions |
|
pos = {'A': (0.5, 1), 'B': (0, 0), 'C': (1, 0)} |
|
|
|
# Save basic graph |
|
filepath = save_graph_visualization(G, "1_triangle_basic", |
|
"Triangle Graph - Complete Symmetry", pos) |
|
print(f"\n✓ Saved: {filepath}") |
|
|
|
analyzer = AutomorphismAnalyzer() |
|
result = analyzer.describe_automorphisms(G) |
|
|
|
print(f"\nNumber of automorphisms: {result['count']}") |
|
print("\nAll automorphisms:") |
|
for i, auto in enumerate(result['automorphisms'], 1): |
|
print(f" {i}. {auto}") |
|
|
|
# Visualize a rotation automorphism |
|
rotation_auto = {'A': 'B', 'B': 'C', 'C': 'A'} |
|
if rotation_auto in result['automorphisms']: |
|
filepath = visualize_automorphism(G, pos, rotation_auto, |
|
"1_triangle_rotation", |
|
"Triangle: Rotation Automorphism (A→B→C→A)") |
|
print(f"✓ Saved: {filepath}") |
|
|
|
# Visualize orbits |
|
orbits = analyzer.find_orbits(G) |
|
print(f"\nOrbits (equivalent vertices):") |
|
for i, orbit in enumerate(orbits, 1): |
|
print(f" Orbit {i}: {orbit}") |
|
|
|
# Color nodes by orbit - all nodes in same orbit get same color |
|
orbit_colors = {} |
|
colors = ['yellow', 'lightcoral', 'lightgreen', 'lightblue', 'lavender'] |
|
for orbit_idx, orbit in enumerate(orbits): |
|
color = colors[orbit_idx % len(colors)] |
|
for node in orbit: |
|
orbit_colors[node] = color |
|
|
|
filepath = save_graph_visualization(G, "1_triangle_orbits", |
|
"Triangle: All vertices in ONE orbit (all yellow = same equivalence class)", |
|
pos, highlight_nodes=orbit_colors) |
|
print(f"✓ Saved: {filepath}") |
|
|
|
print("\nInterpretation:") |
|
print(" - 6 automorphisms (3! = 6) because triangle has full symmetry") |
|
print(" - All 3 vertices in same orbit (can be mapped to each other)") |
|
print(" - Can rotate: A→B→C→A or reflect: A↔B, C stays") |
|
|
|
|
|
def example_2_path(): |
|
""" |
|
Example 2: Path Graph |
|
Reflection symmetry only |
|
""" |
|
print("\n" + "="*70) |
|
print("EXAMPLE 2: Path (Chain)") |
|
print("="*70) |
|
print("\nGraph structure:") |
|
print(" A---B---C---D") |
|
print("\nEnds (A, D) are equivalent, middles (B, C) are equivalent") |
|
|
|
G = nx.Graph() |
|
G.add_edges_from([('A', 'B'), ('B', 'C'), ('C', 'D')]) |
|
|
|
# Fixed positions for path |
|
pos = {'A': (0, 0), 'B': (1, 0), 'C': (2, 0), 'D': (3, 0)} |
|
|
|
filepath = save_graph_visualization(G, "2_path_basic", |
|
"Path Graph - Reflection Symmetry", pos) |
|
print(f"\n✓ Saved: {filepath}") |
|
|
|
analyzer = AutomorphismAnalyzer() |
|
result = analyzer.describe_automorphisms(G) |
|
|
|
print(f"\nNumber of automorphisms: {result['count']}") |
|
print("\nAll automorphisms:") |
|
for i, auto in enumerate(result['automorphisms'], 1): |
|
print(f" {i}. {auto}") |
|
|
|
# Visualize reflection |
|
reflection = {'A': 'D', 'B': 'C', 'C': 'B', 'D': 'A'} |
|
if reflection in result['automorphisms']: |
|
filepath = visualize_automorphism(G, pos, reflection, |
|
"2_path_reflection", |
|
"Path: Reflection Automorphism") |
|
print(f"✓ Saved: {filepath}") |
|
|
|
orbits = analyzer.find_orbits(G) |
|
print(f"\nOrbits (equivalent vertices):") |
|
for i, orbit in enumerate(orbits, 1): |
|
print(f" Orbit {i}: {orbit}") |
|
|
|
# Color by orbit - each orbit gets one color |
|
orbit_colors = {} |
|
colors = ['lightcoral', 'lightgreen', 'lightblue', 'yellow'] |
|
for orbit_idx, orbit in enumerate(orbits): |
|
color = colors[orbit_idx % len(colors)] |
|
for node in orbit: |
|
orbit_colors[node] = color |
|
|
|
filepath = save_graph_visualization(G, "2_path_orbits", |
|
"Path: Two orbits (one color per orbit)", |
|
pos, highlight_nodes=orbit_colors) |
|
print(f"✓ Saved: {filepath}") |
|
|
|
print("\nInterpretation:") |
|
print(" - 2 automorphisms: identity and reflection") |
|
print(" - Orbit {A, D}: the two endpoints are equivalent") |
|
print(" - Orbit {B, C}: the two middle vertices are equivalent") |
|
|
|
|
|
def example_3_star(): |
|
""" |
|
Example 3: Star Graph |
|
Center vs periphery vertices |
|
""" |
|
print("\n" + "="*70) |
|
print("EXAMPLE 3: Star Graph") |
|
print("="*70) |
|
print("\nGraph structure:") |
|
print(" B") |
|
print(" |") |
|
print(" C---A---D") |
|
print(" |") |
|
print(" E") |
|
print("\nA is the center (degree 4), others are leaves (degree 1)") |
|
|
|
G = nx.Graph() |
|
G.add_edges_from([('A', 'B'), ('A', 'C'), ('A', 'D'), ('A', 'E')]) |
|
|
|
# Star layout |
|
pos = { |
|
'A': (0, 0), |
|
'B': (0, 1), |
|
'C': (-1, 0), |
|
'D': (1, 0), |
|
'E': (0, -1) |
|
} |
|
|
|
filepath = save_graph_visualization(G, "3_star_basic", |
|
"Star Graph - Center vs Periphery", pos) |
|
print(f"\n✓ Saved: {filepath}") |
|
|
|
analyzer = AutomorphismAnalyzer() |
|
result = analyzer.describe_automorphisms(G) |
|
|
|
print(f"\nNumber of automorphisms: {result['count']}") |
|
print(f"\nFirst 5 automorphisms (of {result['count']}):") |
|
for i, auto in enumerate(result['automorphisms'][:5], 1): |
|
print(f" {i}. {auto}") |
|
|
|
# Show one permutation of leaves |
|
if len(result['automorphisms']) > 1: |
|
perm_auto = result['automorphisms'][1] |
|
filepath = visualize_automorphism(G, pos, perm_auto, |
|
"3_star_permutation", |
|
"Star: Leaves can be permuted, center stays fixed") |
|
print(f"✓ Saved: {filepath}") |
|
|
|
orbits = analyzer.find_orbits(G) |
|
print(f"\nOrbits:") |
|
for i, orbit in enumerate(orbits, 1): |
|
print(f" Orbit {i}: {orbit}") |
|
|
|
# Highlight orbits |
|
orbit_colors = {} |
|
for orbit in orbits: |
|
if 'A' in orbit: |
|
for node in orbit: |
|
orbit_colors[node] = 'gold' |
|
else: |
|
for node in orbit: |
|
orbit_colors[node] = 'lightblue' |
|
|
|
filepath = save_graph_visualization(G, "3_star_orbits", |
|
"Star: Two orbits (gold=center, blue=leaves)", |
|
pos, highlight_nodes=orbit_colors) |
|
print(f"✓ Saved: {filepath}") |
|
|
|
print("\nInterpretation:") |
|
print(f" - {result['count']} automorphisms (4! = 24)") |
|
print(" - Center A must stay as A (unique degree 4)") |
|
print(" - Leaves {B,C,D,E} can be permuted in any way") |
|
print(" - Two orbits: {A} and {B,C,D,E}") |
|
|
|
|
|
def example_4_asymmetric(): |
|
""" |
|
Example 4: Asymmetric Graph |
|
Only trivial automorphism (identity) |
|
""" |
|
print("\n" + "="*70) |
|
print("EXAMPLE 4: Asymmetric Graph") |
|
print("="*70) |
|
print("\nGraph structure:") |
|
print(" A---B---C---D") |
|
print(" |") |
|
print(" E") |
|
print("\nAll vertices have different 'views' - no symmetry") |
|
print("Degrees: A=1, B=3, C=2, D=1, E=1") |
|
print("B is unique (degree 3), C is unique (degree 2 in middle)") |
|
|
|
G = nx.Graph() |
|
G.add_edges_from([('A', 'B'), ('B', 'C'), ('C', 'D'), ('B', 'E')]) |
|
|
|
# Fixed positions |
|
pos = {'A': (0, 1), 'B': (1, 1), 'C': (2, 1), 'D': (3, 1), 'E': (1, 0)} |
|
|
|
filepath = save_graph_visualization(G, "4_asymmetric_basic", |
|
"Asymmetric Graph - No Symmetry", pos) |
|
print(f"\n✓ Saved: {filepath}") |
|
|
|
analyzer = AutomorphismAnalyzer() |
|
result = analyzer.describe_automorphisms(G) |
|
|
|
print(f"\nNumber of automorphisms: {result['count']}") |
|
print("\nAutomorphisms:") |
|
for i, auto in enumerate(result['automorphisms'], 1): |
|
print(f" {i}. {auto}") |
|
|
|
orbits = analyzer.find_orbits(G) |
|
print(f"\nOrbits:") |
|
for i, orbit in enumerate(orbits, 1): |
|
print(f" Orbit {i}: {orbit}") |
|
|
|
# Color by orbit - each orbit gets its own color |
|
orbit_colors = {} |
|
colors = ['lightcoral', 'lightgreen', 'lightblue', 'yellow', 'lavender'] |
|
for orbit_idx, orbit in enumerate(orbits): |
|
color = colors[orbit_idx % len(colors)] |
|
for node in orbit: |
|
orbit_colors[node] = color |
|
|
|
filepath = save_graph_visualization(G, "4_asymmetric_orbits", |
|
"Asymmetric: Each vertex in its own orbit (5 different colors)", |
|
pos, highlight_nodes=orbit_colors) |
|
print(f"✓ Saved: {filepath}") |
|
|
|
print("\nInterpretation:") |
|
print(" - Only 1 automorphism: identity (every vertex maps to itself)") |
|
print(" - Each vertex is in its own orbit (5 separate orbits)") |
|
print(" - Graph has NO symmetry - it's asymmetric") |
|
print(" - Why? Each vertex has a unique 'structural fingerprint':") |
|
print(" • A: degree 1, neighbor has degree 3") |
|
print(" • B: degree 3 (unique!)") |
|
print(" • C: degree 2, one neighbor degree 3, other degree 1") |
|
print(" • D: degree 1, neighbor has degree 2") |
|
print(" • E: degree 1, neighbor has degree 3") |
|
print(" - Even though A and E both have degree 1, their neighborhoods differ") |
|
|
|
|
|
def example_5_square(): |
|
""" |
|
Example 5: Square (4-cycle) |
|
Rotations and reflections |
|
""" |
|
print("\n" + "="*70) |
|
print("EXAMPLE 5: Square (4-cycle)") |
|
print("="*70) |
|
print("\nGraph structure:") |
|
print(" A---B") |
|
print(" | |") |
|
print(" D---C") |
|
print("\nCan rotate 90°, 180°, 270° and reflect horizontally/vertically") |
|
|
|
G = nx.Graph() |
|
G.add_edges_from([('A', 'B'), ('B', 'C'), ('C', 'D'), ('D', 'A')]) |
|
|
|
# Square positions |
|
pos = {'A': (0, 1), 'B': (1, 1), 'C': (1, 0), 'D': (0, 0)} |
|
|
|
filepath = save_graph_visualization(G, "5_square_basic", |
|
"Square Graph - Rotations & Reflections", pos) |
|
print(f"\n✓ Saved: {filepath}") |
|
|
|
analyzer = AutomorphismAnalyzer() |
|
result = analyzer.describe_automorphisms(G) |
|
|
|
print(f"\nNumber of automorphisms: {result['count']}") |
|
print("\nAll automorphisms:") |
|
for i, auto in enumerate(result['automorphisms'], 1): |
|
description = "" |
|
if auto == {'A': 'A', 'B': 'B', 'C': 'C', 'D': 'D'}: |
|
description = " (identity)" |
|
elif auto == {'A': 'B', 'B': 'C', 'C': 'D', 'D': 'A'}: |
|
description = " (rotate 90° clockwise)" |
|
elif auto == {'A': 'C', 'B': 'D', 'C': 'A', 'D': 'B'}: |
|
description = " (rotate 180°)" |
|
elif auto == {'A': 'D', 'B': 'A', 'C': 'B', 'D': 'C'}: |
|
description = " (rotate 270° clockwise)" |
|
print(f" {i}. {auto}{description}") |
|
|
|
# Visualize 90-degree rotation |
|
rotation_90 = {'A': 'B', 'B': 'C', 'C': 'D', 'D': 'A'} |
|
if rotation_90 in result['automorphisms']: |
|
filepath = visualize_automorphism(G, pos, rotation_90, |
|
"5_square_rotation", |
|
"Square: 90° Clockwise Rotation") |
|
print(f"✓ Saved: {filepath}") |
|
|
|
orbits = analyzer.find_orbits(G) |
|
print(f"\nOrbits:") |
|
for i, orbit in enumerate(orbits, 1): |
|
print(f" Orbit {i}: {sorted(orbit)}") |
|
|
|
# Color by orbit - all nodes in same orbit get same color |
|
orbit_colors = {} |
|
colors = ['lavender', 'lightcoral', 'lightgreen', 'lightblue'] |
|
for orbit_idx, orbit in enumerate(orbits): |
|
color = colors[orbit_idx % len(colors)] |
|
for node in orbit: |
|
orbit_colors[node] = color |
|
|
|
filepath = save_graph_visualization(G, "5_square_orbits", |
|
"Square: All vertices in ONE orbit (all lavender = fully symmetric)", |
|
pos, highlight_nodes=orbit_colors) |
|
print(f"✓ Saved: {filepath}") |
|
|
|
print("\nInterpretation:") |
|
print(" - 8 automorphisms: 4 rotations + 4 reflections") |
|
print(" - All vertices in one orbit (completely symmetric)") |
|
print(" - This is the dihedral group D4") |
|
|
|
|
|
def example_6_circuit_implications(): |
|
""" |
|
Example 6: Why automorphisms matter for circuits |
|
""" |
|
print("\n" + "="*70) |
|
print("EXAMPLE 6: Circuit Design Implications") |
|
print("="*70) |
|
|
|
print("\nScenario: Two circuit subcircuits") |
|
print("\nCircuit 1: NAND gate") |
|
print(" in1 ----\\") |
|
print(" NAND ---- out") |
|
print(" in2 ----/") |
|
|
|
C1 = nx.Graph([('in1', 'nand'), ('in2', 'nand'), ('nand', 'out')]) |
|
|
|
# Circuit layout positions |
|
pos1 = {'in1': (0, 1), 'in2': (0, 0), 'nand': (1, 0.5), 'out': (2, 0.5)} |
|
|
|
filepath = save_graph_visualization(C1, "6_circuit_nand", |
|
"NAND Gate Circuit", pos1) |
|
print(f"\n✓ Saved: {filepath}") |
|
|
|
print("\nCircuit 2: NOR gate with ground") |
|
print(" in1 ----\\") |
|
print(" NOR ---- out") |
|
print(" in2 ----/") |
|
print(" |") |
|
print(" gnd") |
|
|
|
C2 = nx.Graph([('in1', 'nor'), ('in2', 'nor'), ('nor', 'out'), ('nor', 'gnd')]) |
|
|
|
pos2 = {'in1': (0, 1.5), 'in2': (0, 0.5), 'nor': (1, 1), 'out': (2, 1), 'gnd': (1, -0.5)} |
|
|
|
filepath = save_graph_visualization(C2, "6_circuit_nor", |
|
"NOR Gate with Ground", pos2) |
|
print(f"✓ Saved: {filepath}") |
|
|
|
analyzer = AutomorphismAnalyzer() |
|
|
|
print("\n" + "-"*70) |
|
print("Circuit 1 (NAND) Analysis:") |
|
print("-"*70) |
|
result1 = analyzer.describe_automorphisms(C1) |
|
print(f"Automorphisms: {result1['count']}") |
|
orbits1 = analyzer.find_orbits(C1) |
|
print("Orbits:") |
|
for orbit in orbits1: |
|
print(f" {sorted(orbit)}") |
|
|
|
# Highlight input swap automorphism |
|
if result1['count'] > 1: |
|
swap_auto = result1['automorphisms'][1] |
|
filepath = visualize_automorphism(C1, pos1, swap_auto, |
|
"6_circuit_nand_swap", |
|
"NAND: Inputs are interchangeable (commutative)") |
|
print(f"✓ Saved: {filepath}") |
|
|
|
# Highlight orbits |
|
orbit_colors1 = {} |
|
colors = ['lightcoral', 'lightgreen', 'lightblue', 'yellow'] |
|
for i, orbit in enumerate(orbits1): |
|
for node in orbit: |
|
orbit_colors1[node] = colors[i % len(colors)] |
|
|
|
filepath = save_graph_visualization(C1, "6_circuit_nand_orbits", |
|
"NAND Orbits: Inputs in same orbit (interchangeable)", |
|
pos1, highlight_nodes=orbit_colors1) |
|
print(f"✓ Saved: {filepath}") |
|
|
|
print("\nInterpretation:") |
|
print(" - 2 automorphisms: can swap in1 ↔ in2") |
|
print(" - Inputs are interchangeable (commutative)") |
|
print(" - {in1, in2} form one orbit") |
|
print(" - {nand} and {out} each in their own orbit") |
|
|
|
print("\n" + "-"*70) |
|
print("Circuit 2 (NOR with ground) Analysis:") |
|
print("-"*70) |
|
result2 = analyzer.describe_automorphisms(C2) |
|
print(f"Automorphisms: {result2['count']}") |
|
orbits2 = analyzer.find_orbits(C2) |
|
print("Orbits:") |
|
for orbit in orbits2: |
|
print(f" {sorted(orbit)}") |
|
|
|
# Highlight orbits |
|
orbit_colors2 = {} |
|
for i, orbit in enumerate(orbits2): |
|
for node in orbit: |
|
orbit_colors2[node] = colors[i % len(colors)] |
|
|
|
filepath = save_graph_visualization(C2, "6_circuit_nor_orbits", |
|
"NOR Orbits: More structure, more separate orbits", |
|
pos2, highlight_nodes=orbit_colors2) |
|
print(f"✓ Saved: {filepath}") |
|
|
|
print("\nInterpretation:") |
|
print(" - 2 automorphisms: can swap in1 ↔ in2") |
|
print(" - Inputs still interchangeable") |
|
print(" - out and gnd are NOT interchangeable (different roles)") |
|
|
|
print("\n" + "="*70) |
|
print("KEY INSIGHTS FOR SEMICONDUCTOR DESIGN:") |
|
print("="*70) |
|
print("1. Automorphism count = measure of circuit symmetry") |
|
print("2. Orbits = functionally equivalent pins/nodes") |
|
print("3. High automorphism count → harder for isomorphism testing") |
|
print(" (VF2 must explore all symmetric variations)") |
|
print("4. Canonicalization exploits automorphisms to create unique form") |
|
print("5. Understanding symmetry helps optimize circuit layout") |
|
|
|
|
|
def main(): |
|
"""Run all examples.""" |
|
print("\n" + "█"*70) |
|
print("GRAPH AUTOMORPHISMS: COMPLETE GUIDE WITH VISUALIZATIONS") |
|
print("█"*70) |
|
|
|
print("\nDEFINITION:") |
|
print("An automorphism is a mapping of a graph to itself that preserves edges.") |
|
print("It's an 'isomorphism from a graph to itself' - a symmetry operation.") |
|
print("\nFormally: A permutation π of vertices where:") |
|
print(" (u,v) is an edge ⟺ (π(u), π(v)) is an edge") |
|
|
|
print("\nWHY IT MATTERS:") |
|
print("• Automorphism group size = measure of graph symmetry") |
|
print("• Orbits identify 'equivalent' vertices under symmetry") |
|
print("• Critical for canonical labeling algorithms (like Nauty)") |
|
print("• Affects isomorphism testing complexity") |
|
print("• Helps understand circuit symmetry in EDA applications") |
|
|
|
print(f"\nAll visualizations will be saved to: {OUTPUT_DIR}/") |
|
print("="*70) |
|
|
|
# Run examples |
|
example_1_triangle() |
|
example_2_path() |
|
example_3_star() |
|
example_4_asymmetric() |
|
example_5_square() |
|
example_6_circuit_implications() |
|
|
|
print("\n" + "="*70) |
|
print("SUMMARY") |
|
print("="*70) |
|
print("\nAutomorphism Count Interpretation:") |
|
print(" • Count = 1 → Asymmetric (no symmetry)") |
|
print(" • Count = 2 → One symmetry (e.g., reflection)") |
|
print(" • Count = n! → Completely symmetric (all vertices equivalent)") |
|
print(" • Count between → Partial symmetry") |
|
print("\nOrbits:") |
|
print(" • Vertices in same orbit are 'structurally equivalent'") |
|
print(" • Can be swapped without changing graph structure") |
|
print(" • Useful for: pin assignment, layout optimization, equivalence checking") |
|
print("\nFor Your Interview:") |
|
print(" • Automorphisms = graph symmetries") |
|
print(" • Used by Nauty for efficient canonical labeling") |
|
print(" • High symmetry → harder isomorphism testing (more cases)") |
|
print(" • Understanding orbits helps identify equivalent components") |
|
|
|
print(f"\n✓ All visualizations saved to: {os.path.abspath(OUTPUT_DIR)}/") |
|
print("="*70) |
|
|
|
|
|
if __name__ == "__main__": |
|
main() |