Skip to content

Instantly share code, notes, and snippets.

@mdmitry1
Last active December 13, 2025 09:25
Show Gist options
  • Select an option

  • Save mdmitry1/eadd1de72d8cfe43c436b7b9b057aeae to your computer and use it in GitHub Desktop.

Select an option

Save mdmitry1/eadd1de72d8cfe43c436b7b9b057aeae to your computer and use it in GitHub Desktop.

General Performance Comparison

VF2 advantages

VF2 tends to perform better on graphs that are sparse (fewer connections), small, or have a regular structure (e.g., 2D meshes or bounded valence graphs with low valence). In some cases, Nauty cannot find a solution for these regular graphs if they exceed a certain size (a few dozen nodes).

Nauty advantages

Nauty (via pynauty) is generally the better algorithm for dense or large randomly connected graphs.

#!/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()
#!/usr/bin/python3.14
"""
Converting Graph Algorithms from CPU to GPU: Comprehensive Guide
Critical for semiconductor EDA where graphs can have millions of nodes
(circuit netlists, routing graphs, polygon operations).
This guide covers frameworks, patterns, and practical examples.
"""
import numpy as np
import time
from typing import List, Tuple
import matplotlib.pyplot as plt
import os
OUTPUT_DIR = "gpu_graph_conversion"
if not os.path.exists(OUTPUT_DIR):
os.makedirs(OUTPUT_DIR)
def print_section(title):
"""Print formatted section header."""
print("\n" + "="*70)
print(title)
print("="*70)
def explain_gpu_frameworks():
"""Explain different GPU computing frameworks."""
print_section("GPU COMPUTING FRAMEWORKS FOR GRAPH ALGORITHMS")
frameworks = {
"CUDA (NVIDIA)": {
"Language": "C++/CUDA C",
"Best for": "Maximum performance, custom kernels",
"Learning curve": "Steep",
"Portability": "NVIDIA GPUs only",
"Use when": "Need absolute best performance, complex algorithms",
"Example libs": "cuGraph, Gunrock, RAPIDS"
},
"CuPy (Python)": {
"Language": "Python (NumPy-like API)",
"Best for": "Array operations, easy migration from NumPy",
"Learning curve": "Easy (if you know NumPy)",
"Portability": "NVIDIA GPUs only",
"Use when": "Quick prototyping, array-heavy algorithms",
"Example libs": "CuPy, cuGraph (Python bindings)"
},
"Numba (Python)": {
"Language": "Python with decorators",
"Best for": "JIT compilation, custom kernels in Python",
"Learning curve": "Medium",
"Portability": "NVIDIA GPUs (CUDA), some AMD support",
"Use when": "Want Python simplicity with GPU speed",
"Example libs": "Numba CUDA, numba.cuda"
},
"PyTorch/TensorFlow": {
"Language": "Python",
"Best for": "Graph Neural Networks, ML on graphs",
"Learning curve": "Medium",
"Portability": "NVIDIA, AMD (ROCm), Apple Silicon",
"Use when": "ML-based graph algorithms, GNNs",
"Example libs": "PyTorch Geometric, DGL"
},
"OpenCL": {
"Language": "C/C++",
"Best for": "Cross-platform GPU computing",
"Learning curve": "Steep",
"Portability": "NVIDIA, AMD, Intel, mobile GPUs",
"Use when": "Need multi-vendor support",
"Example libs": "ViennaCL, CLBlast"
},
"Vulkan Compute": {
"Language": "C++/GLSL",
"Best for": "Modern cross-platform compute",
"Learning curve": "Very steep",
"Portability": "Excellent (all modern GPUs)",
"Use when": "Cutting-edge, cross-platform needs",
"Example libs": "Vulkan SDK"
}
}
print("\nFRAMEWORK COMPARISON:\n")
for fw, details in frameworks.items():
print(f"▼ {fw}")
for key, value in details.items():
print(f" • {key}: {value}")
print()
print("\nRECOMMENDED APPROACH FOR GRAPH ALGORITHMS:")
print("━" * 70)
print("1. START: Numba or CuPy (Python, rapid prototyping)")
print("2. OPTIMIZE: CUDA C++ (for production, maximum performance)")
print("3. SCALE: cuGraph library (pre-optimized graph algorithms)")
print("4. ML: PyTorch Geometric (if using GNNs)")
print("━" * 70)
def explain_conversion_patterns():
"""Explain common patterns for converting graph algorithms to GPU."""
print_section("CONVERSION PATTERNS: CPU → GPU")
print("\n1. DATA PARALLELISM (Easiest)")
print("━" * 70)
print("CPU Pattern:")
print("""
for node in graph.nodes():
result[node] = compute(node)
""")
print("\nGPU Pattern:")
print("""
@cuda.jit
def kernel(nodes, results):
idx = cuda.grid(1)
if idx < len(nodes):
results[idx] = compute(nodes[idx])
# Launch: One thread per node
threads_per_block = 256
blocks = (num_nodes + threads_per_block - 1) // threads_per_block
kernel[blocks, threads_per_block](nodes, results)
""")
print("✓ Suitable for: Independent per-node operations")
print("✓ Examples: Degree calculation, node coloring, pagerank updates")
print("\n2. EDGE PARALLELISM")
print("━" * 70)
print("CPU Pattern:")
print("""
for u, v in graph.edges():
update(u, v)
""")
print("\nGPU Pattern:")
print("""
# Store edges as arrays: src[], dst[]
@cuda.jit
def edge_kernel(src, dst, data):
idx = cuda.grid(1)
if idx < len(src):
u = src[idx]
v = dst[idx]
process_edge(u, v, data)
""")
print("✓ Suitable for: Edge-centric algorithms")
print("✓ Examples: Edge contraction, triangle counting")
print("\n3. LEVEL SYNCHRONOUS (BFS-style)")
print("━" * 70)
print("CPU Pattern:")
print("""
frontier = [start_node]
while frontier:
next_frontier = []
for node in frontier:
for neighbor in graph.neighbors(node):
if not visited[neighbor]:
visited[neighbor] = True
next_frontier.append(neighbor)
frontier = next_frontier
""")
print("\nGPU Pattern:")
print("""
# Process entire frontier in parallel per level
while frontier_size > 0:
kernel[blocks, threads](
frontier, graph, visited, next_frontier
)
cuda.synchronize()
frontier, next_frontier = next_frontier, frontier
""")
print("✓ Suitable for: Traversal algorithms")
print("✓ Examples: BFS, SSSP (Bellman-Ford), connected components")
print("\n4. WORK QUEUE (Dynamic parallelism)")
print("━" * 70)
print("CPU Pattern:")
print("""
queue = [initial_work]
while queue:
work_item = queue.pop()
new_work = process(work_item)
queue.extend(new_work)
""")
print("\nGPU Pattern:")
print("""
# Use atomic operations for queue management
@cuda.jit
def work_kernel(queue, queue_size, results):
idx = cuda.grid(1)
if idx < queue_size[0]:
item = queue[idx]
new_items = process(item)
# Atomically add to queue
for new_item in new_items:
pos = cuda.atomic.add(queue_size, 0, 1)
queue[pos] = new_item
""")
print("✓ Suitable for: Irregular workloads")
print("✓ Examples: DFS-based algorithms, backtracking")
print("\n5. REDUCTION (Aggregation)")
print("━" * 70)
print("CPU Pattern:")
print("""
total = 0
for node in graph.nodes():
total += compute(node)
""")
print("\nGPU Pattern:")
print("""
# Use parallel reduction
@cuda.reduce
def sum_reduce(a, b):
return a + b
result = sum_reduce(node_values)
""")
print("✓ Suitable for: Computing aggregates")
print("✓ Examples: Diameter, clustering coefficient, centrality measures")
def explain_data_structures():
"""Explain GPU-friendly graph data structures."""
print_section("GPU-FRIENDLY GRAPH DATA STRUCTURES")
print("\n❌ BAD: Traditional Adjacency Lists (CPU-style)")
print("━" * 70)
print("""
graph = {
0: [1, 2, 3],
1: [0, 4],
2: [0, 5, 6],
...
}
""")
print("Problems:")
print(" • Pointer-based → poor GPU memory access")
print(" • Variable-length lists → hard to parallelize")
print(" • Random access → cache misses")
print("\n✓ GOOD: Compressed Sparse Row (CSR)")
print("━" * 70)
print("""
# Example: Graph with edges 0→1, 0→2, 1→3, 2→3
offset = [0, 2, 3, 4] # Where each node's neighbors start
neighbors = [1, 2, 3, 3] # All neighbors concatenated
# Node 0's neighbors: neighbors[offset[0]:offset[1]] = [1, 2]
# Node 1's neighbors: neighbors[offset[1]:offset[2]] = [3]
# Node 2's neighbors: neighbors[offset[2]:offset[3]] = [3]
""")
print("Advantages:")
print(" ✓ Contiguous memory → coalesced access")
print(" ✓ Easy to copy to GPU")
print(" ✓ Efficient parallel iteration")
print("\nUsed by: cuGraph, Gunrock, all high-performance graph libraries")
print("\n✓ GOOD: Edge List (for edge-parallel algorithms)")
print("━" * 70)
print("""
src = [0, 0, 1, 2] # Source nodes
dst = [1, 2, 3, 3] # Destination nodes
weights = [1.0, 2.0, 1.5, 0.5] # Optional
""")
print("Advantages:")
print(" ✓ Simple, easy to parallelize over edges")
print(" ✓ Good for edge-centric algorithms")
print(" ✓ Easy to partition for multi-GPU")
print("\n✓ GOOD: Adjacency Matrix (for dense graphs)")
print("━" * 70)
print("""
# n×n matrix where A[i][j] = 1 if edge exists
adj_matrix = np.array([
[0, 1, 1, 0],
[1, 0, 0, 1],
[1, 0, 0, 1],
[0, 1, 1, 0]
])
""")
print("Advantages:")
print(" ✓ Perfect for dense graphs")
print(" ✓ Can use GPU matrix operations (cuBLAS)")
print(" ✓ Great for GNNs and algebraic methods")
print("Disadvantages:")
print(" ✗ O(n²) memory for sparse graphs")
def create_numba_example():
"""Create a working Numba GPU example."""
print_section("PRACTICAL EXAMPLE: BFS in Numba CUDA")
print("\nInstallation:")
print(" pip install numba")
print(" (Requires NVIDIA GPU with CUDA)")
print("\nCPU Version (Traditional BFS):")
print("━" * 70)
cpu_code = '''
def bfs_cpu(graph, start):
"""Traditional CPU BFS."""
visited = {start}
queue = [start]
distances = {start: 0}
while queue:
node = queue.pop(0)
for neighbor in graph[node]:
if neighbor not in visited:
visited.add(neighbor)
distances[neighbor] = distances[node] + 1
queue.append(neighbor)
return distances
'''
print(cpu_code)
print("\nGPU Version (Numba CUDA):")
print("━" * 70)
gpu_code = '''
import numba
from numba import cuda
import numpy as np
@cuda.jit
def bfs_kernel(offset, neighbors, current_level, next_level,
distances, level):
"""
Process one BFS level in parallel.
Each thread handles one node from current frontier.
"""
idx = cuda.grid(1)
if idx < len(current_level) and current_level[idx] != -1:
node = current_level[idx]
# Explore all neighbors
start = offset[node]
end = offset[node + 1]
for i in range(start, end):
neighbor = neighbors[i]
# If not visited, add to next level
old_dist = cuda.atomic.cas(distances, neighbor, -1, level + 1)
if old_dist == -1:
# Successfully claimed this node
pos = cuda.atomic.add(next_level, 0, 1)
if pos < len(next_level) - 1:
next_level[pos + 1] = neighbor
def bfs_gpu(offset, neighbors, start, num_nodes):
"""GPU BFS using level-synchronous approach."""
# Initialize
distances = np.full(num_nodes, -1, dtype=np.int32)
distances[start] = 0
current_level = np.full(num_nodes, -1, dtype=np.int32)
current_level[0] = start
current_size = 1
level = 0
while current_size > 0:
next_level = np.full(num_nodes, -1, dtype=np.int32)
next_level[0] = 0 # Counter for next level size
# Launch kernel
threads_per_block = 256
blocks = (current_size + threads_per_block - 1) // threads_per_block
bfs_kernel[blocks, threads_per_block](
offset, neighbors, current_level[:current_size],
next_level, distances, level
)
cuda.synchronize()
# Get next level size
current_size = int(next_level[0])
current_level = next_level[1:current_size+1].copy()
level += 1
return distances
'''
print(gpu_code)
print("\nKey Differences:")
print(" 1. CSR representation (offset, neighbors arrays)")
print(" 2. Level-synchronous (process entire frontier in parallel)")
print(" 3. Atomic operations (cuda.atomic.cas for claiming nodes)")
print(" 4. Explicit memory management")
def create_comparison_chart():
"""Create algorithm suitability chart."""
print_section("ALGORITHM SUITABILITY FOR GPU")
algorithms = [
("Breadth-First Search", "Excellent", "Level-synchronous, high parallelism"),
("Depth-First Search", "Poor", "Inherently sequential"),
("PageRank", "Excellent", "Iterative, data-parallel"),
("Connected Components", "Good", "Can use label propagation"),
("Shortest Path (SSSP)", "Good", "Bellman-Ford parallelizes well"),
("Triangle Counting", "Excellent", "Edge-parallel, high throughput"),
("Graph Isomorphism", "Poor", "Backtracking, irregular work"),
("Betweenness Centrality", "Good", "Multiple BFS, embarrassingly parallel"),
("Community Detection", "Good", "Iterative refinement"),
("Graph Neural Networks", "Excellent", "Matrix ops, perfectly suited"),
]
print("\n{:<30} {:<15} {}".format("Algorithm", "GPU Suitability", "Reason"))
print("─" * 80)
for algo, suit, reason in algorithms:
print(f"{algo:<30} {suit:<15} {reason}")
print("\n\nGENERAL RULES:")
print("━" * 70)
print("✓ GOOD for GPU:")
print(" • Regular structure (grids, trees)")
print(" • High parallelism (many independent operations)")
print(" • Data-parallel (same operation on many elements)")
print(" • Iterative algorithms (many small steps)")
print(" • Dense computation per memory access")
print("\n✗ BAD for GPU:")
print(" • Irregular workload (varying work per thread)")
print(" • Pointer chasing (random memory access)")
print(" • Small graphs (< 10K nodes, overhead dominates)")
print(" • Inherently sequential algorithms")
print(" • Frequent host-device transfers")
def explain_pitfalls():
"""Explain common pitfalls and solutions."""
print_section("COMMON PITFALLS & SOLUTIONS")
pitfalls = [
{
"Pitfall": "Race Conditions",
"Problem": "Multiple threads updating same memory location",
"Example": "Two threads incrementing counter simultaneously",
"Solution": "Use atomic operations (cuda.atomic.add, cas, etc.)",
"Code": "cuda.atomic.add(array, index, value)"
},
{
"Pitfall": "Memory Coalescing",
"Problem": "Random memory access patterns kill performance",
"Example": "Accessing array[thread_id * stride] with large stride",
"Solution": "Access contiguous memory, use CSR format",
"Code": "# Good: array[thread_id] # Bad: array[thread_id * 1000]"
},
{
"Pitfall": "Thread Divergence",
"Problem": "Threads in same warp taking different branches",
"Example": "if (thread_id % 2 == 0) { ... } else { ... }",
"Solution": "Minimize conditionals, restructure algorithm",
"Code": "# Reorganize data so threads execute same path"
},
{
"Pitfall": "Memory Transfer Overhead",
"Problem": "PCIe transfer slower than computation",
"Example": "Copying data CPU→GPU for each iteration",
"Solution": "Keep data on GPU, batch transfers",
"Code": "# Do multiple iterations on GPU before copying back"
},
{
"Pitfall": "Load Imbalance",
"Problem": "Some threads do much more work than others",
"Example": "Nodes with degree 1 vs degree 1000",
"Solution": "Work stealing, dynamic load balancing",
"Code": "# Use work queue, assign work dynamically"
},
]
for i, p in enumerate(pitfalls, 1):
print(f"\n{i}. {p['Pitfall']}")
print("─" * 70)
print(f"Problem: {p['Problem']}")
print(f"Example: {p['Example']}")
print(f"Solution: {p['Solution']}")
print(f"Code: {p['Code']}")
def create_decision_tree():
"""Create decision tree for choosing GPU approach."""
fig, ax = plt.subplots(figsize=(16, 10))
ax.axis('off')
ax.text(0.5, 0.95, "GPU Graph Algorithm Decision Tree",
ha='center', fontsize=18, fontweight='bold',
transform=ax.transAxes)
# Decision tree as text
tree_text = """
┌─ START: Should I use GPU for this graph algorithm? ─┐
├─ Q1: Graph size?
│ ├─ < 10K nodes → ✗ Stay on CPU (overhead > benefit)
│ ├─ 10K-1M nodes → ✓ GPU beneficial
│ └─ > 1M nodes → ✓✓ GPU essential
├─ Q2: Algorithm type?
│ ├─ Data-parallel (BFS, PageRank) → ✓ Perfect for GPU
│ ├─ Edge-parallel (Triangle count) → ✓ Perfect for GPU
│ ├─ Sequential (DFS) → ✗ Stay on CPU
│ └─ Irregular (Backtracking) → ✗ GPU difficult
├─ Q3: Development time vs Performance?
│ ├─ Need fast prototyping → Use Numba or CuPy
│ ├─ Need production performance → Use CUDA C++
│ ├─ Already have data in PyTorch → Use PyTorch Geometric
│ └─ Need standard algorithms → Use cuGraph library
├─ Q4: Hardware constraints?
│ ├─ NVIDIA only → CUDA/cuGraph (best performance)
│ ├─ Multi-vendor → OpenCL or Vulkan
│ └─ No GPU → Consider multi-core CPU parallelism
└─ Q5: Graph structure?
├─ Dense graph → Use matrix operations (cuBLAS)
├─ Sparse graph → Use CSR format
├─ Power-law degree → Need load balancing
└─ Regular structure → Standard parallelization
"""
ax.text(0.05, 0.85, tree_text, ha='left', va='top',
fontsize=10, family='monospace',
transform=ax.transAxes,
bbox=dict(boxstyle='round', facecolor='lightblue', alpha=0.3))
# Recommendations box
rec_text = """
RECOMMENDED WORKFLOW:
1. PROTOTYPE (Python):
• Use Numba CUDA for custom kernels
• Use CuPy for array operations
• Use cuGraph for standard algorithms
2. OPTIMIZE (if needed):
• Profile with nvprof/Nsight
• Rewrite hotspots in CUDA C++
• Optimize memory access patterns
3. PRODUCTION:
• Use cuGraph library (RAPIDS)
• Consider multi-GPU with Dask
• Benchmark against CPU baseline
"""
ax.text(0.5, 0.12, rec_text, ha='left', va='top',
fontsize=10, family='monospace',
transform=ax.transAxes,
bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5))
filepath = os.path.join(OUTPUT_DIR, "gpu_decision_tree.png")
plt.tight_layout()
plt.savefig(filepath, dpi=150, bbox_inches='tight', facecolor='white')
plt.close()
print(f"\n✓ Decision tree saved: {filepath}")
def main():
"""Generate complete guide."""
print("\n" + "█"*70)
print("CONVERTING GRAPH ALGORITHMS TO GPU: COMPLETE GUIDE")
print("█"*70)
print("\nWHY GPU FOR GRAPH ALGORITHMS?")
print("━" * 70)
print("Modern semiconductor designs:")
print(" • Circuit netlists: millions of nodes (gates, components)")
print(" • Routing graphs: billions of edges (possible connections)")
print(" • Polygon operations: millions of shapes per mask layer")
print("\nGPUs offer:")
print(" • 1000s of cores vs 10s on CPU")
print(" • 10-100x speedup for suitable algorithms")
print(" • Essential for real-time EDA tools")
print("━" * 70)
explain_gpu_frameworks()
explain_conversion_patterns()
explain_data_structures()
create_numba_example()
create_comparison_chart()
explain_pitfalls()
print_section("SEMICONDUCTOR EDA SPECIFIC APPLICATIONS")
print("\nGraph algorithms on GPU in EDA:")
print(" 1. Circuit Netlist Analysis")
print(" • Timing analysis: SSSP on timing graphs")
print(" • Connectivity: Connected components")
print(" • Fanout analysis: BFS from critical nodes")
print()
print(" 2. Routing")
print(" • Path finding: Parallel A* on routing grid")
print(" • Congestion analysis: PageRank on routing graph")
print(" • Layer assignment: Graph coloring")
print()
print(" 3. Placement")
print(" • Partitioning: Spectral methods (GPU matrix ops)")
print(" • Force-directed: Iterative refinement")
print(" • Clustering: Community detection")
print()
print(" 4. Verification")
print(" • DRC: Spatial queries (quadtree/R-tree on GPU)")
print(" • LVS: Graph isomorphism (challenging on GPU)")
print(" • Parasitic extraction: Connected components")
create_decision_tree()
print("\n" + "█"*70)
print("INTERVIEW KEY POINTS")
print("█"*70)
print("\n1. Framework Choice:")
print(" • Prototype: Numba (Python, easy)")
print(" • Production: CUDA C++ (fast)")
print(" • Pre-built: cuGraph (RAPIDS)")
print("\n2. Data Structure:")
print(" • Always use CSR for sparse graphs")
print(" • Edge lists for edge-parallel algorithms")
print(" • Avoid pointer-based structures")
print("\n3. Parallelization Pattern:")
print(" • Data-parallel: One thread per node/edge")
print(" • Level-synchronous: For traversals (BFS)")
print(" • Reduction: For aggregates")
print("\n4. Challenges:")
print(" • Load imbalance (power-law degree distribution)")
print(" • Memory access patterns (coalescing)")
print(" • Thread divergence (minimize conditionals)")
print("\n5. When NOT to use GPU:")
print(" • Small graphs (< 10K nodes)")
print(" • Sequential algorithms (DFS)")
print(" • Frequent CPU-GPU transfers")
print(f"\n✓ All materials saved to: {os.path.abspath(OUTPUT_DIR)}/")
print("="*70)
if __name__ == "__main__":
main()
#!/usr/bin/python3.14
"""
Graph Isomorphism Algorithms on GPU: Analysis and Approaches
Graph isomorphism is one of the HARDEST problems to parallelize on GPU.
This guide explains why and presents research-backed solutions.
"""
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import os
OUTPUT_DIR = "gpu_isomorphism_analysis"
if not os.path.exists(OUTPUT_DIR):
os.makedirs(OUTPUT_DIR)
def print_section(title):
"""Print formatted section header."""
print("\n" + "="*70)
print(title)
print("="*70)
def explain_why_hard():
"""Explain why isomorphism is hard for GPU."""
print_section("WHY GRAPH ISOMORPHISM IS HARD FOR GPU")
print("\n1. INHERENTLY SEQUENTIAL SEARCH")
print("━" * 70)
print("VF2 Algorithm (traditional):")
print("""
def vf2_recursive(mapping, candidates):
if complete(mapping):
return True # Found isomorphism
for candidate in candidates:
if feasible(candidate, mapping):
mapping.add(candidate)
if vf2_recursive(mapping, next_candidates):
return True # Success in subtree
mapping.remove(candidate) # Backtrack
return False # No solution in this branch
""")
print("Problems for GPU:")
print(" ✗ Backtracking: Inherently sequential, each step depends on previous")
print(" ✗ Early termination: Can't parallelize when you need to stop at first success")
print(" ✗ Irregular branching: Different paths have vastly different lengths")
print(" ✗ Thread divergence: Some threads explore deep, others terminate early")
print("\n2. DYNAMIC WORKLOAD")
print("━" * 70)
print("Search tree characteristics:")
print(" • Highly irregular depth (some branches: 2 steps, others: 1000)")
print(" • Unpredictable branching (1 candidate vs 100 candidates)")
print(" • Cannot predict work distribution a priori")
print(" • Load imbalance: Some threads idle while others work")
print("\n3. MEMORY ACCESS PATTERNS")
print("━" * 70)
print("Isomorphism checking requires:")
print(" • Random access to adjacency information")
print(" • Checking neighbors of arbitrary vertices")
print(" • No spatial locality or regular patterns")
print(" • Poor cache utilization on GPU")
print("\n4. STATE MANAGEMENT")
print("━" * 70)
print("Each search branch needs:")
print(" • Its own mapping state (which vertices paired)")
print(" • Visited/candidate sets")
print(" • Backtracking history")
print(" • Memory grows with search depth")
print(" • GPU has limited memory per thread")
def explain_what_can_be_parallelized():
"""Explain which parts CAN be parallelized."""
print_section("WHAT *CAN* BE PARALLELIZED")
print("\n1. WEISFEILER-LEMAN (WL) REFINEMENT ✓✓")
print("━" * 70)
print("WL is HIGHLY parallelizable!")
print("""
# Each iteration is data-parallel
@cuda.jit
def wl_refinement_kernel(colors, neighbors, neighbor_colors, new_colors):
node = cuda.grid(1)
if node < len(colors):
# Collect neighbor colors
neighbor_signature = []
for neighbor in get_neighbors(node):
neighbor_signature.append(colors[neighbor])
# Compute new color (hash of signature)
new_colors[node] = hash(colors[node], sorted(neighbor_signature))
# Perfect for GPU: Same operation on all nodes in parallel
""")
print("Advantages:")
print(" ✓ Data-parallel: Same operation on every node")
print(" ✓ Regular structure: Fixed iterations")
print(" ✓ Predictable workload: Equal work per node")
print(" ✓ 10-100x speedup possible")
print("\nUse case:")
print(" → Fast pre-filtering before exact matching")
print(" → Reject 95%+ non-isomorphic pairs instantly")
print(" → Only run expensive VF2 on remaining candidates")
print("\n2. CANONICAL LABELING (PARTIAL) ✓")
print("━" * 70)
print("Some parts of Nauty/Traces can be parallelized:")
print(" ✓ Automorphism orbit computation: Parallel BFS/DFS")
print(" ✓ Vertex invariant calculation: Data-parallel")
print(" ✓ Partition refinement: Similar to WL")
print(" ✗ Backtracking search: Still sequential")
print("\nHybrid approach:")
print(" • GPU: Compute vertex invariants in parallel")
print(" • GPU: Refine partitions in parallel")
print(" • CPU: Handle backtracking search tree")
print("\n3. SUBGRAPH ISOMORPHISM (EMBARRASSINGLY PARALLEL) ✓✓")
print("━" * 70)
print("If you need to check MULTIPLE pairs:")
print("""
# Check if query graph is isomorphic to any of 10,000 database graphs
@cuda.jit
def parallel_subgraph_search(query, database, results):
graph_id = cuda.grid(1)
if graph_id < len(database):
# Each thread checks one database graph independently
results[graph_id] = check_isomorphism(query, database[graph_id])
# Perfect parallelization: No communication between threads
""")
print("Advantages:")
print(" ✓ Embarrassingly parallel: Independent checks")
print(" ✓ Perfect for library search (circuit pattern matching)")
print(" ✓ 1000x speedup possible (1000 graphs in parallel)")
print("\n4. SIMILARITY METRICS (INSTEAD OF EXACT) ✓✓")
print("━" * 70)
print("For approximate matching, GPU excels:")
print(" • Graph kernels: Fast similarity computation")
print(" • GNN embeddings: Neural network on GPU")
print(" • Spectral methods: Matrix operations (cuBLAS)")
print(" • Edit distance: Dynamic programming (parallelizable)")
def research_approaches():
"""Discuss research approaches for GPU isomorphism."""
print_section("RESEARCH APPROACHES: GPU ISOMORPHISM")
print("\n1. PARALLEL BACKTRACKING WITH WORK STEALING")
print("━" * 70)
print("Idea: Parallelize different branches of search tree")
print("""
Initial mapping:
/ | \\
A B C ← Assign to different GPU threads
/|\\ | |
... ... ...
Each thread explores a subtree independently.
Use work stealing when threads finish early.
""")
print("Challenges:")
print(" • Load imbalance: Some branches terminate fast")
print(" • Synchronization overhead")
print(" • Limited speedup (2-4x typical)")
print("\nResearch papers:")
print(" • 'Parallel Subgraph Isomorphism on GPU' (2012)")
print(" • 'GPU-based Graph Pattern Matching' (2015)")
print("\n2. HYBRID CPU-GPU APPROACH")
print("━" * 70)
print("Best practical solution:")
print("""
CPU:
├─ WL refinement (GPU) → Filter non-isomorphic
├─ Vertex invariants (GPU) → Order vertices
├─ Initial pruning (GPU) → Reduce search space
└─ VF2 backtracking (CPU) → Exact matching
GPU handles parallelizable preprocessing,
CPU handles sequential search.
""")
print("Speedup: 5-20x on large databases")
print("\n3. GRAPH NEURAL NETWORKS (GNN)")
print("━" * 70)
print("Modern approach: Learn to predict isomorphism")
print("""
# Train GNN to embed graphs
embedding1 = GNN(graph1) # GPU
embedding2 = GNN(graph2) # GPU
similarity = cosine_similarity(embedding1, embedding2)
if similarity > threshold:
# Likely isomorphic, verify with VF2
return exact_check(graph1, graph2)
else:
return False # Definitely not isomorphic
""")
print("Advantages:")
print(" ✓ GNNs run excellently on GPU")
print(" ✓ Fast approximate check")
print(" ✓ Handles large graphs")
print("Disadvantages:")
print(" ✗ Not exact (false negatives possible)")
print(" ✗ Requires training data")
print("\nResearch:")
print(" • 'Graph Matching Networks' (2019)")
print(" • 'SimGNN: Learning Graph Similarity' (2020)")
print("\n4. NAUTY ON GPU (LIMITED SUCCESS)")
print("━" * 70)
print("Attempts to port Nauty to GPU:")
print(" • Partition refinement: Successfully parallelized")
print(" • Invariant computation: Successfully parallelized")
print(" • Backtracking: Remains on CPU")
print(" • Overall speedup: 2-3x (modest)")
print("\nConclusion: Not worth the complexity for most cases")
def practical_recommendations():
"""Provide practical recommendations."""
print_section("PRACTICAL RECOMMENDATIONS FOR SEMICONDUCTOR EDA")
print("\nSCENARIO 1: Single Pair Isomorphism Check")
print("━" * 70)
print("Question: Are circuit A and circuit B isomorphic?")
print("\nRecommendation: CPU (VF2 or Nauty)")
print(" • Single pair → no parallelism to exploit")
print(" • GPU overhead > benefit")
print(" • Just use optimized CPU library")
print("\nSCENARIO 2: Library Search (1 vs Many)")
print("━" * 70)
print("Question: Is this subcircuit in our library of 10,000 cells?")
print("\nRecommendation: HYBRID")
print("""
Step 1: WL hash on GPU (10,000 hashes in parallel)
↓ Rejects 95%+ in milliseconds
Step 2: VF2 on CPU (remaining ~500 candidates)
↓ Exact verification
Step 3: Found match or confirmed novel design
""")
print("Expected speedup: 10-50x vs pure CPU")
print("\nSCENARIO 3: Database Search (Many vs Many)")
print("━" * 70)
print("Question: Find all duplicate circuits in database")
print("\nRecommendation: GPU EMBARRASSINGLY PARALLEL")
print("""
# 1,000,000 pairwise comparisons
@cuda.jit
def pairwise_check(circuits, pairs, results):
pair_id = cuda.grid(1)
i, j = pairs[pair_id]
# Each thread checks one pair independently
results[pair_id] = wl_hash(circuits[i]) == wl_hash(circuits[j])
# Then CPU verifies candidates with VF2
""")
print("Expected speedup: 100-1000x")
print("\nSCENARIO 4: Real-time Circuit Recognition")
print("━" * 70)
print("Question: Identify circuit patterns during layout")
print("\nRecommendation: GNN on GPU")
print("""
# Pre-trained GNN model
while designing:
new_subcircuit = extract_region(layout)
embedding = GNN(new_subcircuit) # GPU: 1-5ms
matches = find_similar_embeddings(embedding, library)
if matches:
suggest_optimization(matches)
""")
print("Advantages:")
print(" • Real-time (< 10ms)")
print(" • Approximate is fine for suggestions")
print(" • GPU handles thousands of queries/second")
def create_comparison_chart():
"""Create visual comparison of approaches."""
fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(16, 12))
fig.suptitle("Graph Isomorphism on GPU: Approaches Comparison",
fontsize=18, fontweight='bold')
# Chart 1: Speedup potential
approaches = ['VF2 Parallel', 'WL Filter', 'Library Search', 'GNN Approx']
speedups = [2, 100, 1000, 500]
colors = ['red', 'orange', 'green', 'blue']
ax1.bar(approaches, speedups, color=colors, alpha=0.7)
ax1.set_ylabel('Typical Speedup (vs CPU)', fontweight='bold')
ax1.set_title('GPU Speedup Potential', fontweight='bold')
ax1.set_yscale('log')
ax1.grid(True, alpha=0.3, axis='y')
for i, v in enumerate(speedups):
ax1.text(i, v, f'{v}x', ha='center', va='bottom', fontweight='bold')
# Chart 2: Difficulty
difficulties = [9, 3, 2, 6]
ax2.bar(approaches, difficulties, color=colors, alpha=0.7)
ax2.set_ylabel('Implementation Difficulty (1-10)', fontweight='bold')
ax2.set_title('Implementation Complexity', fontweight='bold')
ax2.set_ylim(0, 10)
ax2.grid(True, alpha=0.3, axis='y')
for i, v in enumerate(difficulties):
ax2.text(i, v, str(v), ha='center', va='bottom', fontweight='bold')
# Chart 3: Use cases
ax3.axis('off')
ax3.text(0.5, 0.95, 'Best Use Cases', ha='center', fontsize=14,
fontweight='bold', transform=ax3.transAxes)
use_cases = [
('VF2 Parallel', 'Research/academic\nLimited practical value', 'red'),
('WL Filter', 'Pre-filtering large databases\nReject non-isomorphic fast', 'orange'),
('Library Search', 'Circuit pattern matching\n1 vs 10,000 comparisons', 'green'),
('GNN Approx', 'Real-time suggestions\nApproximate matching', 'blue')
]
y = 0.8
for name, desc, color in use_cases:
ax3.text(0.1, y, f'• {name}:', fontweight='bold', color=color,
transform=ax3.transAxes, fontsize=11)
ax3.text(0.15, y-0.06, desc, transform=ax3.transAxes,
fontsize=10, style='italic', color='gray')
y -= 0.2
# Chart 4: Decision matrix
ax4.axis('off')
ax4.text(0.5, 0.95, 'Decision Matrix', ha='center', fontsize=14,
fontweight='bold', transform=ax4.transAxes)
matrix_text = """
╔══════════════════════╦══════════════════════╗
║ SINGLE PAIR CHECK ║ → CPU (VF2/Nauty) ║
║ (1 vs 1) ║ No GPU benefit ║
╠══════════════════════╬══════════════════════╣
║ LIBRARY SEARCH ║ → Hybrid (WL + VF2) ║
║ (1 vs N, N=10K+) ║ 10-50x speedup ║
╠══════════════════════╬══════════════════════╣
║ DATABASE DEDUP ║ → GPU parallel ║
║ (N vs N, N=1000+) ║ 100-1000x speedup ║
╠══════════════════════╬══════════════════════╣
║ REAL-TIME MATCHING ║ → GNN on GPU ║
║ (streaming) ║ 500x speedup ║
╠══════════════════════╬══════════════════════╣
║ EXACT VERIFICATION ║ → CPU always ║
║ (legal/critical) ║ Backtracking ║
╚══════════════════════╩══════════════════════╝
"""
ax4.text(0.05, 0.85, matrix_text, ha='left', va='top',
transform=ax4.transAxes, family='monospace', fontsize=9,
bbox=dict(boxstyle='round', facecolor='lightyellow', alpha=0.5))
plt.tight_layout()
filepath = os.path.join(OUTPUT_DIR, "isomorphism_comparison.png")
plt.savefig(filepath, dpi=150, bbox_inches='tight')
plt.close()
print(f"\n✓ Comparison chart saved: {filepath}")
def example_wl_gpu_pseudocode():
"""Show how WL would work on GPU."""
print_section("EXAMPLE: WL REFINEMENT ON GPU (PSEUDOCODE)")
print("\nWeisfeiler-Leman is the MOST PRACTICAL GPU approach")
print("━" * 70)
print("\nCPU Version:")
print("""
def wl_refine_cpu(graph, colors, iterations=10):
for iteration in range(iterations):
new_colors = {}
for node in graph.nodes():
# Get neighbor colors
neighbor_colors = [colors[n] for n in graph.neighbors(node)]
# Create signature
signature = (colors[node], tuple(sorted(neighbor_colors)))
# Hash to new color
new_colors[node] = hash(signature)
colors = new_colors
return colors
""")
print("\nGPU Version (Numba CUDA):")
print("""
from numba import cuda
import numpy as np
@cuda.jit
def wl_kernel(offset, neighbors, colors, neighbor_counts, new_colors):
'''
offset: CSR offset array
neighbors: CSR neighbor array
colors: Current color of each node
neighbor_counts: Scratch space for neighbor color counts
new_colors: Output colors
'''
node = cuda.grid(1)
if node < len(colors):
# Get this node's neighbors
start = offset[node]
end = offset[node + 1]
# Collect neighbor colors (simplified: just sum for hashing)
neighbor_color_sum = 0
for i in range(start, end):
neighbor = neighbors[i]
neighbor_color_sum += colors[neighbor]
# Compute new color (hash)
new_colors[node] = hash((colors[node], neighbor_color_sum))
def wl_refine_gpu(offset, neighbors, num_nodes, iterations=10):
'''Run WL refinement on GPU.'''
# Initialize colors (degree)
colors = np.array([offset[i+1] - offset[i] for i in range(num_nodes)],
dtype=np.int32)
# Allocate GPU memory
d_offset = cuda.to_device(offset)
d_neighbors = cuda.to_device(neighbors)
d_colors = cuda.to_device(colors)
d_new_colors = cuda.device_array(num_nodes, dtype=np.int32)
threads_per_block = 256
blocks = (num_nodes + threads_per_block - 1) // threads_per_block
for iteration in range(iterations):
# Launch kernel
wl_kernel[blocks, threads_per_block](
d_offset, d_neighbors, d_colors, d_new_colors
)
# Swap buffers
d_colors, d_new_colors = d_new_colors, d_colors
# Copy result back
return d_colors.copy_to_host()
# Usage:
# color_signature1 = wl_refine_gpu(offset1, neighbors1, n1)
# color_signature2 = wl_refine_gpu(offset2, neighbors2, n2)
# if color_signature1 != color_signature2:
# return False # Definitely not isomorphic
""")
print("\nKey Points:")
print(" ✓ Each thread processes one node independently")
print(" ✓ No synchronization within iteration")
print(" ✓ Coalesced memory access (CSR format)")
print(" ✓ Expected speedup: 50-100x for large graphs")
def literature_references():
"""Comprehensive literature references."""
print_section("LITERATURE REFERENCES")
print("\n═══════════════════════════════════════════════════════════════════════")
print("FOUNDATIONAL GRAPH ISOMORPHISM ALGORITHMS")
print("═══════════════════════════════════════════════════════════════════════")
print("\n[1] VF2 Algorithm (CPU Baseline)")
print(" Cordella, L.P., Foggia, P., Sansone, C., Vento, M.")
print(" 'A (Sub)Graph Isomorphism Algorithm for Matching Large Graphs'")
print(" IEEE Transactions on Pattern Analysis and Machine Intelligence")
print(" Vol. 26, No. 10, pp. 1367-1372, 2004")
print(" DOI: 10.1109/TPAMI.2004.75")
print(" ★ Most widely used subgraph isomorphism algorithm")
print("\n[2] Nauty - Canonical Labeling")
print(" McKay, B.D., Piperno, A.")
print(" 'Practical Graph Isomorphism, II'")
print(" Journal of Symbolic Computation, Vol. 60, pp. 94-112, 2014")
print(" DOI: 10.1016/j.jsc.2013.09.003")
print(" ★ State-of-the-art for exact isomorphism")
print(" Software: http://pallini.di.uniroma1.it/")
print("\n[3] Weisfeiler-Leman Algorithm")
print(" Weisfeiler, B., Leman, A.")
print(" 'The reduction of a graph to canonical form and the algebra")
print(" which appears therein'")
print(" Nauchno-Technicheskaya Informatsia, 2(9):12-16, 1968")
print(" ★ Foundation for modern GNN architectures")
print("\n═══════════════════════════════════════════════════════════════════════")
print("GPU GRAPH PROCESSING - GENERAL")
print("═══════════════════════════════════════════════════════════════════════")
print("\n[4] Gunrock: GPU Graph Analytics")
print(" Wang, Y., Davidson, A., Pan, Y., Wu, Y., Riffel, A., Owens, J.D.")
print(" 'Gunrock: A High-Performance Graph Processing Library on the GPU'")
print(" ACM SIGPLAN Symposium on Principles and Practice of Parallel")
print(" Programming (PPoPP), 2016")
print(" DOI: 10.1145/2851141.2851145")
print(" GitHub: https://github.com/gunrock/gunrock")
print("\n[5] cuGraph: RAPIDS GPU Graph Analytics")
print(" NVIDIA RAPIDS Team")
print(" 'cuGraph: GPU-Accelerated Graph Analytics'")
print(" Documentation: https://docs.rapids.ai/api/cugraph/stable/")
print(" GitHub: https://github.com/rapidsai/cugraph")
print(" ★ Production-ready GPU graph library")
print("\n[6] Hornet: Dynamic GPU Graph Data Structure")
print(" Busato, F., Green, O., Bombieri, N., Bader, D.A.")
print(" 'Hornet: An Efficient Data Structure for Dynamic Sparse Graphs")
print(" and Matrices on GPUs'")
print(" IEEE High Performance Extreme Computing (HPEC), 2018")
print(" DOI: 10.1109/HPEC.2018.8547566")
print("\n═══════════════════════════════════════════════════════════════════════")
print("GPU SUBGRAPH ISOMORPHISM & PATTERN MATCHING")
print("═══════════════════════════════════════════════════════════════════════")
print("\n[7] GPU Subgraph Isomorphism (Early Work)")
print(" Katsaloulis, P., Verykios, V.S., Theodoridis, Y.")
print(" 'GPU Accelerated Sub-graph Pattern Matching'")
print(" International Conference on Database and Expert Systems")
print(" Applications (DEXA), pp. 207-211, 2012")
print(" DOI: 10.1109/DEXA.2012.52")
print(" ★ First attempt at GPU VF2")
print("\n[8] GSI: GPU-based Subgraph Isomorphism")
print(" Su, J., Ouyang, Y., Zhuang, Q., Chen, Y., Peng, B., Hu, Y.")
print(" 'GSI: GPU-friendly Subgraph Isomorphism'")
print(" IEEE International Parallel and Distributed Processing")
print(" Symposium (IPDPS), pp. 83-93, 2019")
print(" DOI: 10.1109/IPDPS.2019.00019")
print(" ★ Modern approach with work-efficient strategies")
print("\n[9] GpSM: GPU-based Subgraph Matching")
print(" Shi, X., Luo, X.")
print(" 'GpSM: A GPU-based Approach for Subgraph Matching'")
print(" IEEE International Conference on Big Data, pp. 1369-1378, 2016")
print(" DOI: 10.1109/BigData.2016.7840744")
print("\n[10] CuSha: Vertex-Centric Graph Processing")
print(" Khorasani, F., Vora, K., Gupta, R., Bhuyan, L.N.")
print(" 'CuSha: Vertex-centric graph processing on GPUs'")
print(" ACM SIGPLAN Symposium on High-Performance Parallel and")
print(" Distributed Computing (HPDC), pp. 239-252, 2014")
print(" DOI: 10.1145/2600212.2600227")
print("\n═══════════════════════════════════════════════════════════════════════")
print("GRAPH NEURAL NETWORKS & LEARNING-BASED APPROACHES")
print("═══════════════════════════════════════════════════════════════════════")
print("\n[11] Graph Matching Networks")
print(" Li, Y., Gu, C., Dullien, T., Vinyals, O., Kohli, P.")
print(" 'Graph Matching Networks for Learning the Similarity of")
print(" Graph Structured Objects'")
print(" International Conference on Machine Learning (ICML), 2019")
print(" arXiv: 1904.12787")
print(" ★ Foundation for learned graph matching")
print("\n[12] SimGNN: Learning Graph Similarity")
print(" Bai, Y., Ding, H., Bian, S., Chen, T., Sun, Y., Wang, W.")
print(" 'SimGNN: A Neural Network Approach to Fast Graph Similarity")
print(" Computation'")
print(" ACM International Conference on Web Search and Data Mining")
print(" (WSDM), pp. 384-392, 2019")
print(" DOI: 10.1145/3289600.3290967")
print("\n[13] Weisfeiler-Leman Goes Neural")
print(" Morris, C., Ritzert, M., Fey, M., Hamilton, W.L.,")
print(" Lenssen, J.E., Rattan, G., Grohe, M.")
print(" 'Weisfeiler and Leman Go Neural: Higher-order Graph Neural")
print(" Networks'")
print(" AAAI Conference on Artificial Intelligence, 2019")
print(" arXiv: 1810.02244")
print(" ★ Connects WL to GNN expressiveness")
print("\n[14] PyTorch Geometric")
print(" Fey, M., Lenssen, J.E.")
print(" 'Fast Graph Representation Learning with PyTorch Geometric'")
print(" ICLR Workshop on Representation Learning on Graphs")
print(" and Manifolds, 2019")
print(" arXiv: 1903.02428")
print(" GitHub: https://github.com/pyg-team/pytorch_geometric")
print("\n═══════════════════════════════════════════════════════════════════════")
print("SEMICONDUCTOR EDA APPLICATIONS")
print("═══════════════════════════════════════════════════════════════════════")
print("\n[15] GPU-Accelerated Circuit Simulation")
print(" Zhou, D., Hu, J., Ye, S.")
print(" 'GPU-Accelerated Analog Circuit Simulation'")
print(" IEEE/ACM International Conference on Computer-Aided Design")
print(" (ICCAD), pp. 700-707, 2015")
print(" DOI: 10.1109/ICCAD.2015.7372639")
print("\n[16] Parallel Graph Analysis in EDA")
print(" Papa, D.A., Markov, I.L.")
print(" 'Hypergraph Partitioning and Clustering'")
print(" Handbook of Approximation Algorithms and Metaheuristics,")
print(" Chapter 61, 2007")
print(" ★ Graph algorithms in VLSI CAD")
print("\n[17] Machine Learning for EDA")
print(" Huang, G., Hu, J., He, Y., Liu, J., Ma, M., Shen, Z.,")
print(" Wu, J., Xu, Y., Zhang, H., Zhong, K., et al.")
print(" 'Machine Learning for Electronic Design Automation: A Survey'")
print(" ACM Transactions on Design Automation of Electronic Systems")
print(" (TODAES), Vol. 26, No. 5, Article 40, 2021")
print(" DOI: 10.1145/3451179")
print("\n═══════════════════════════════════════════════════════════════════════")
print("GPU PROGRAMMING & OPTIMIZATION")
print("═══════════════════════════════════════════════════════════════════════")
print("\n[18] CUDA Programming Guide")
print(" NVIDIA Corporation")
print(" 'CUDA C++ Programming Guide'")
print(" Latest version: https://docs.nvidia.com/cuda/cuda-c-programming-guide/")
print(" ★ Essential reference for CUDA development")
print("\n[19] Numba: JIT Compiler for Python")
print(" Lam, S.K., Pitrou, A., Seibert, S.")
print(" 'Numba: A LLVM-based Python JIT Compiler'")
print(" ACM SIGPLAN Workshop on Libraries, Languages and Compilers")
print(" for Array Programming (ARRAY), 2015")
print(" DOI: 10.1145/2833157.2833162")
print(" Documentation: https://numba.pydata.org/")
print("\n[20] GPU Graph Processing Survey")
print(" Shi, X., Zheng, Z., Zhou, Y., Jin, H., He, L., Liu, B., Hua, Q.S.")
print(" 'Graph Processing on GPUs: A Survey'")
print(" ACM Computing Surveys, Vol. 50, No. 6, Article 81, 2018")
print(" DOI: 10.1145/3128571")
print(" ★ Comprehensive survey of GPU graph algorithms")
print("\n═══════════════════════════════════════════════════════════════════════")
print("BOOKS")
print("═══════════════════════════════════════════════════════════════════════")
print("\n[B1] Graph Algorithms in the Language of Linear Algebra")
print(" Kepner, J., Gilbert, J. (Editors)")
print(" SIAM, 2011")
print(" ISBN: 978-0-898719-90-1")
print(" ★ Foundation for GPU matrix-based graph algorithms")
print("\n[B2] Programming Massively Parallel Processors")
print(" Kirk, D.B., Hwu, W.W.")
print(" Morgan Kaufmann, 4th Edition, 2022")
print(" ISBN: 978-0-323-91231-0")
print(" ★ Best textbook for learning GPU programming")
print("\n[B3] Graph Isomorphism: Problem Structure and Complexity")
print(" Grohe, M., Schweitzer, P.")
print(" Lecture Notes in Computer Science (LNCS), Vol. 10941, 2018")
print(" ★ Theoretical foundations")
print("\n═══════════════════════════════════════════════════════════════════════")
print("ONLINE RESOURCES & TOOLS")
print("═══════════════════════════════════════════════════════════════════════")
print("\n[W1] NetworkX - Python Graph Library")
print(" https://networkx.org/")
print(" ★ Reference implementation of many algorithms")
print("\n[W2] igraph - High-Performance Graph Library")
print(" https://igraph.org/")
print(" ★ C core with Python/R bindings")
print("\n[W3] Boost Graph Library (BGL)")
print(" https://www.boost.org/doc/libs/release/libs/graph/")
print(" ★ C++ template library, includes VF2")
print("\n[W4] Deep Graph Library (DGL)")
print(" https://www.dgl.ai/")
print(" ★ GPU-accelerated GNN framework")
print("\n[W5] Papers With Code - Graph Matching")
print(" https://paperswithcode.com/task/graph-matching")
print(" ★ Latest research with code implementations")
print("\n═══════════════════════════════════════════════════════════════════════")
print("CITATION ADVICE FOR YOUR INTERVIEW")
print("═══════════════════════════════════════════════════════════════════════")
print("\nKey papers to mention:")
print(" • VF2 [1]: 'The standard CPU baseline, Cordella et al. 2004'")
print(" • Nauty [2]: 'McKay & Piperno's canonical labeling, state-of-the-art'")
print(" • GSI [8]: 'Modern GPU subgraph matching, Su et al. 2019'")
print(" • SimGNN [12]: 'Learning-based approach, Bai et al. 2019'")
print(" • cuGraph [5]: 'NVIDIA RAPIDS production library'")
print("\nHow to reference in interview:")
print(" 'Research by Su et al. (IPDPS 2019) showed that GPU subgraph")
print(" isomorphism achieves modest speedups (2-4x) due to irregular")
print(" workloads, but pre-filtering with WL refinement can give 50-100x")
print(" speedup for the filtering stage.'")
print("\n═══════════════════════════════════════════════════════════════════════")
print("RECENT CONFERENCES TO FOLLOW (2023-2024)")
print("═══════════════════════════════════════════════════════════════════════")
print("\n• IPDPS - Int'l Parallel and Distributed Processing Symposium")
print("• PPoPP - Principles and Practice of Parallel Programming")
print("• HPDC - High-Performance Parallel and Distributed Computing")
print("• SC - Supercomputing Conference")
print("• ICCAD - Int'l Conference on Computer-Aided Design")
print("• DAC - Design Automation Conference")
print("• ICML/NeurIPS - For GNN advances")
def create_bibliography_file():
"""Create a formatted bibliography file."""
filepath = os.path.join(OUTPUT_DIR, "bibliography.txt")
with open(filepath, 'w', encoding='utf-8') as f:
f.write("="*70 + "\n")
f.write("GRAPH ISOMORPHISM ON GPU - COMPREHENSIVE BIBLIOGRAPHY\n")
f.write("="*70 + "\n\n")
f.write("FOUNDATIONAL ALGORITHMS\n")
f.write("-"*70 + "\n\n")
f.write("[1] Cordella, L.P., Foggia, P., Sansone, C., Vento, M.\n")
f.write(" 'A (Sub)Graph Isomorphism Algorithm for Matching Large Graphs'\n")
f.write(" IEEE TPAMI, Vol. 26, No. 10, pp. 1367-1372, 2004\n")
f.write(" DOI: 10.1109/TPAMI.2004.75\n\n")
f.write("[2] McKay, B.D., Piperno, A.\n")
f.write(" 'Practical Graph Isomorphism, II'\n")
f.write(" Journal of Symbolic Computation, Vol. 60, pp. 94-112, 2014\n")
f.write(" DOI: 10.1016/j.jsc.2013.09.003\n")
f.write(" Software: http://pallini.di.uniroma1.it/\n\n")
f.write("\nGPU GRAPH PROCESSING\n")
f.write("-"*70 + "\n\n")
f.write("[3] Wang, Y., et al.\n")
f.write(" 'Gunrock: A High-Performance Graph Processing Library on the GPU'\n")
f.write(" PPoPP 2016\n")
f.write(" GitHub: https://github.com/gunrock/gunrock\n\n")
f.write("[4] NVIDIA RAPIDS cuGraph\n")
f.write(" https://docs.rapids.ai/api/cugraph/stable/\n")
f.write(" GitHub: https://github.com/rapidsai/cugraph\n\n")
f.write("\nGPU SUBGRAPH ISOMORPHISM\n")
f.write("-"*70 + "\n\n")
f.write("[5] Su, J., Ouyang, Y., Zhuang, Q., Chen, Y., Peng, B., Hu, Y.\n")
f.write(" 'GSI: GPU-friendly Subgraph Isomorphism'\n")
f.write(" IEEE IPDPS, pp. 83-93, 2019\n")
f.write(" DOI: 10.1109/IPDPS.2019.00019\n\n")
f.write("\nLEARNING-BASED APPROACHES\n")
f.write("-"*70 + "\n\n")
f.write("[6] Bai, Y., et al.\n")
f.write(" 'SimGNN: A Neural Network Approach to Fast Graph Similarity'\n")
f.write(" WSDM 2019\n")
f.write(" DOI: 10.1145/3289600.3290967\n\n")
f.write("[7] Li, Y., et al.\n")
f.write(" 'Graph Matching Networks for Learning the Similarity of\n")
f.write(" Graph Structured Objects'\n")
f.write(" ICML 2019\n")
f.write(" arXiv: 1904.12787\n\n")
f.write("\nSURVEYS & BOOKS\n")
f.write("-"*70 + "\n\n")
f.write("[8] Shi, X., et al.\n")
f.write(" 'Graph Processing on GPUs: A Survey'\n")
f.write(" ACM Computing Surveys, Vol. 50, No. 6, 2018\n")
f.write(" DOI: 10.1145/3128571\n\n")
f.write("[9] Kirk, D.B., Hwu, W.W.\n")
f.write(" 'Programming Massively Parallel Processors'\n")
f.write(" Morgan Kaufmann, 4th Edition, 2022\n\n")
f.write("\nONLINE RESOURCES\n")
f.write("-"*70 + "\n\n")
f.write("• PyTorch Geometric: https://github.com/pyg-team/pytorch_geometric\n")
f.write("• NetworkX: https://networkx.org/\n")
f.write("• Numba CUDA: https://numba.pydata.org/\n")
f.write("• Papers with Code: https://paperswithcode.com/task/graph-matching\n")
print(f"\n✓ Bibliography saved: {filepath}")
return filepath
def main():
"""Run complete analysis with all sections."""
print("\n" + "="*70)
print("GRAPH ISOMORPHISM ON GPU: COMPLETE ANALYSIS")
print("="*70)
print("\n## TL;DR: EXECUTIVE SUMMARY")
print("━" * 70)
print("❌ Exact isomorphism (VF2): Poor GPU fit (2-4x speedup, not worth it)")
print("✓ WL pre-filtering: Excellent GPU fit (50-100x speedup)")
print("✓ Library search: Perfect parallelization (1000x speedup)")
print("✓ GNN approximate: Great for real-time (500x speedup)")
print("\nRecommendation: Use hybrid approach (WL on GPU + VF2 on CPU)")
print("━" * 70)
explain_why_hard()
explain_what_can_be_parallelized()
research_approaches()
practical_recommendations()
example_wl_gpu_pseudocode()
create_comparison_chart()
literature_references()
create_bibliography_file()
print_section("INTERVIEW TALKING POINTS")
print("\n1. Why isomorphism is hard for GPU:")
print(" • Backtracking is inherently sequential")
print(" • Irregular workload (load imbalance)")
print(" • Random memory access patterns")
print(" Reference: Su et al. (IPDPS 2019) on GPU subgraph matching")
print("\n2. What CAN be parallelized:")
print(" • WL refinement: Data-parallel (50-100x)")
print(" • Multiple pairs: Embarrassingly parallel (1000x)")
print(" • Preprocessing: Vertex invariants")
print(" Reference: cuGraph library (NVIDIA RAPIDS)")
print("\n3. Practical approach for EDA:")
print(" • Library search: WL filter on GPU, VF2 verify on CPU")
print(" • Single check: Just use CPU (VF2 or Nauty)")
print(" • Real-time: GNN embeddings on GPU")
print(" Reference: Cordella et al. (TPAMI 2004) for VF2")
print("\n4. State of research:")
print(" • Direct VF2 parallelization: Limited success (2-4x)")
print(" • Hybrid approaches: Practical (10-50x)")
print(" • GNN methods: Emerging (approximate but fast)")
print(" Reference: Bai et al. (WSDM 2019) for SimGNN")
print("\n5. Recommendation:")
print(" 'For semiconductor EDA, use WL on GPU for fast pre-filtering,")
print(" then CPU VF2 for exact verification. This hybrid gives 10-50x")
print(" speedup without the complexity of full GPU isomorphism.'")
print(f"\n✓ All materials saved to: {os.path.abspath(OUTPUT_DIR)}/")
print("="*70)
if __name__ == "__main__":
main()
#!/usr/bin/python3.12
"""
Graph Canonicalization Using Various Approaches
Demonstrates Nauty-like canonicalization for circuit isomorphism
NOTE: This uses pynauty if available, otherwise falls back to
NetworkX's weisfeiler_lehman_graph_hash or custom implementation.
To install pynauty (optional):
pip install pynauty
"""
import networkx as nx
from collections import defaultdict
import hashlib
import itertools
class GraphCanonicalizer:
"""
Provides multiple approaches to graph canonicalization.
"""
def __init__(self):
self.has_pynauty = self._check_pynauty()
self.has_networkx_canon = self._check_networkx_canon()
def _check_pynauty(self):
"""Check if pynauty is available."""
try:
import pynauty
return True
except ImportError:
return False
def _check_networkx_canon(self):
"""Check if NetworkX has canonical labeling (v3.0+)."""
try:
from networkx.algorithms import isomorphism
return hasattr(isomorphism, 'weisfeiler_lehman_graph_hash')
except:
return False
def canonicalize_with_pynauty(self, G: nx.Graph):
"""
Use PyNauty for true canonical form (if available).
This is the gold standard for canonicalization.
"""
if not self.has_pynauty:
raise ImportError("pynauty not installed. Install with: pip install pynauty")
import pynauty
# Convert NetworkX graph to pynauty format
# pynauty uses integer vertex labels starting from 0
node_to_int = {node: i for i, node in enumerate(G.nodes())}
n = len(node_to_int)
# Build adjacency dictionary for pynauty
adjacency_dict = {i: [] for i in range(n)}
for u, v in G.edges():
u_int = node_to_int[u]
v_int = node_to_int[v]
adjacency_dict[u_int].append(v_int)
adjacency_dict[v_int].append(u_int)
# Create pynauty graph
g = pynauty.Graph(
number_of_vertices=n,
directed=False,
adjacency_dict=adjacency_dict
)
certificate = pynauty.certificate(g).hex()
canonical_labeling=pynauty.canon_label(g)
canonical_graph = pynauty.Graph(n)
adjacency_dict=g._get_adjacency_dict()
for u in range(n):
for v in adjacency_dict[u]:
# Add edge in canonical graph if (u,v) is an edge in original graph
# and the canonical mapping ensures it's added only once (e.g., u < v)
if u < v: # Avoid adding duplicate edges in undirected graphs
canonical_graph.connect_vertex(canonical_labeling[u], canonical_labeling[v])
canonical_certificate = pynauty.certificate(canonical_graph).hex()
print("Canonization suceeded? ", canonical_certificate == certificate)
# Compute canonical labeling
# Returns: (canonical_labeling, automorphism_group_size, orbits)
automorphism_result = pynauty.autgrp(g)
automorphism_group_size = automorphism_result[1]
orbits = len(set(automorphism_result[3]))
seen_edges = set()
canonical_adjacency_dict=canonical_graph._get_adjacency_dict()
for u in range(n):
for v in canonical_adjacency_dict:
if u == v or (not canonical_graph.directed and (v, u) in seen_edges):
continue
seen_edges.add((u, v))
# Create canonical string (sorted edge list with canonical labels)
canonical_edges = sorted([tuple(sorted([u, v]))
for u, v in seen_edges])
canonical_string = str(canonical_edges)
# Hash for efficient comparison
canonical_hash = hashlib.sha256(canonical_string.encode()).hexdigest()
return {
'canonical_graph': canonical_graph,
'canonical_string': canonical_string,
'canonical_hash': canonical_hash,
'automorphism_group_size': automorphism_group_size,
'orbits': orbits if orbits is not None else None,
'canonical_labeling': tuple(canonical_labeling) if canonical_labeling is not None else None
}
def canonicalize_with_wl_hash(self, G: nx.Graph):
"""
Use Weisfeiler-Leman hash (fast approximation).
Not perfect but very fast and works well in practice.
"""
if self.has_networkx_canon:
# Use NetworkX's built-in WL hash (v3.0+)
try:
wl_hash = nx.weisfeiler_lehman_graph_hash(G)
return {
'canonical_hash': wl_hash,
'method': 'networkx_wl',
'note': 'Approximate - may have false positives'
}
except:
pass
# Fallback: custom WL implementation
return self._custom_wl_hash(G)
def _custom_wl_hash(self, G: nx.Graph, iterations=10):
"""
Custom Weisfeiler-Leman hash implementation.
"""
# Initialize colors by degree
colors = {node: G.degree(node) for node in G.nodes()}
# Iterate to refine colors
for iteration in range(iterations):
new_colors = {}
for node in G.nodes():
# Get sorted neighbor colors
neighbor_colors = sorted([colors[neighbor]
for neighbor in G.neighbors(node)])
# Create new color from old color + neighbor colors
signature = (colors[node], tuple(neighbor_colors))
new_colors[node] = hash(signature)
# Check if converged
if new_colors == colors:
break
colors = new_colors
# Create graph hash from color distribution
color_multiset = tuple(sorted(colors.values()))
graph_hash = hashlib.sha256(str(color_multiset).encode()).hexdigest()
return {
'canonical_hash': graph_hash,
'color_distribution': color_multiset,
'iterations': iteration + 1,
'method': 'custom_wl',
'note': 'Approximate - may have false positives'
}
def canonicalize_small_graph_exact(self, G: nx.Graph):
"""
Brute force exact canonicalization for small graphs (< 10 nodes).
Tries all permutations - only for demonstration!
"""
if G.number_of_nodes() > 10:
raise ValueError("Graph too large for brute force (>10 nodes)")
vertices = list(G.nodes())
n = len(vertices)
min_sequence = None
min_labeling = None
# Try all n! permutations
for perm in itertools.permutations(range(n)):
# Create mapping: original vertex -> new label
mapping = {vertices[i]: perm[i] for i in range(n)}
# Build adjacency matrix with this labeling
matrix = []
for i in range(n):
row = []
for j in range(n):
# Find original vertices
u = vertices[i]
v = vertices[j]
row.append(1 if G.has_edge(u, v) else 0)
matrix.append(row)
# Flatten to sequence
sequence = tuple(matrix[i][j] for i in range(n) for j in range(n))
# Keep lexicographically smallest
if min_sequence is None or sequence < min_sequence:
min_sequence = sequence
min_labeling = perm
# Create canonical graph
mapping = {vertices[i]: min_labeling[i] for i in range(n)}
canonical_graph = nx.relabel_nodes(G, mapping)
# Create canonical string
canonical_edges = sorted([tuple(sorted([u, v]))
for u, v in canonical_graph.edges()])
canonical_string = str((min_sequence, canonical_edges))
canonical_hash = hashlib.sha256(canonical_string.encode()).hexdigest()
return {
'canonical_graph': canonical_graph,
'canonical_string': canonical_string,
'canonical_hash': canonical_hash,
'adjacency_sequence': min_sequence,
'method': 'brute_force_exact'
}
def canonicalize(self, G: nx.Graph, method='auto'):
"""
Canonicalize a graph using the best available method.
Args:
G: NetworkX graph
method: 'auto', 'pynauty', 'wl_hash', 'exact_small'
Returns:
Dictionary with canonical form information
"""
if method == 'auto':
# Choose best available method
if G.number_of_nodes() <= 10:
method = 'exact_small'
elif self.has_pynauty:
method = 'pynauty'
else:
method = 'wl_hash'
if method == 'pynauty':
return self.canonicalize_with_pynauty(G)
elif method == 'wl_hash':
return self.canonicalize_with_wl_hash(G)
elif method == 'exact_small':
return self.canonicalize_small_graph_exact(G)
else:
raise ValueError(f"Unknown method: {method}")
class CanonicalCircuitLibrary:
"""
Circuit library using canonical forms for fast lookup.
"""
def __init__(self, canonicalizer=None):
self.canonicalizer = canonicalizer or GraphCanonicalizer()
self.library = {} # canonical_hash -> list of (circuit_id, graph, metadata)
self.method = 'auto'
def add_circuit(self, circuit_id, graph, metadata=None):
"""Add a circuit to the library."""
result = self.canonicalizer.canonicalize(graph, method=self.method)
canonical_hash = result['canonical_hash']
if canonical_hash not in self.library:
self.library[canonical_hash] = []
# Make a safe copy of result without the graph object
safe_result = {
'canonical_string': result.get('canonical_string'),
'canonical_hash': result.get('canonical_hash'),
'method': result.get('method'),
'automorphism_group_size': result.get('automorphism_group_size'),
'orbits': result.get('orbits'),
'canonical_labeling': result.get('canonical_labeling')
}
self.library[canonical_hash].append({
'circuit_id': circuit_id,
'graph': graph,
'metadata': metadata or {},
'canonical_result': safe_result
})
def find_matches(self, query_graph):
"""
Find all circuits isomorphic to query.
Returns list of matching circuit IDs.
"""
result = self.canonicalizer.canonicalize(query_graph, method=self.method)
canonical_hash = result['canonical_hash']
if canonical_hash in self.library:
matches = self.library[canonical_hash]
# If using WL hash (approximate), verify with VF2
if result.get('method') in ['custom_wl', 'networkx_wl']:
verified_matches = []
for match in matches:
matcher = nx.algorithms.isomorphism.GraphMatcher(
query_graph, match['graph']
)
if matcher.is_isomorphic():
verified_matches.append(match)
return verified_matches
else:
# Exact method, no verification needed
return matches
return []
def demonstrate_canonicalization():
"""Demonstrate different canonicalization approaches."""
print("="*70)
print("GRAPH CANONICALIZATION DEMONSTRATION")
print("="*70)
canonicalizer = GraphCanonicalizer()
print(f"\nAvailable methods:")
print(f" PyNauty (exact): {'✓ Available' if canonicalizer.has_pynauty else '✗ Not installed'}")
print(f" NetworkX WL hash: {'✓ Available' if canonicalizer.has_networkx_canon else '✗ Not available'}")
print(f" Brute force (small graphs): ✓ Always available")
# Create test graphs - isomorphic triangles with different labels
print("\n" + "="*70)
print("TEST: Isomorphic Triangles with Different Labels")
print("="*70)
G1 = nx.Graph()
G1.add_edges_from([('A', 'B'), ('B', 'C'), ('C', 'A')])
G2 = nx.Graph()
G2.add_edges_from([('X', 'Y'), ('Y', 'Z'), ('Z', 'X')])
G3 = nx.Graph()
G3.add_edges_from([('P', 'Q'), ('Q', 'R'), ('R', 'P')])
print(f"\nG1 edges: {list(G1.edges())}")
print(f"G2 edges: {list(G2.edges())}")
print(f"G3 edges: {list(G3.edges())}")
# Test with brute force (always available)
print("\n" + "-"*70)
print("Method: Brute Force Exact Canonicalization")
print("-"*70)
canon1 = canonicalizer.canonicalize(G1, method='exact_small')
canon2 = canonicalizer.canonicalize(G2, method='exact_small')
canon3 = canonicalizer.canonicalize(G3, method='exact_small')
print(f"\nG1 canonical hash: {canon1['canonical_hash'][:16]}...")
print(f"G2 canonical hash: {canon2['canonical_hash'][:16]}...")
print(f"G3 canonical hash: {canon3['canonical_hash'][:16]}...")
print(f"\nG1 adjacency seq: {canon1['adjacency_sequence']}")
print(f"G2 adjacency seq: {canon2['adjacency_sequence']}")
print(f"G3 adjacency seq: {canon3['adjacency_sequence']}")
if canon1['canonical_hash'] == canon2['canonical_hash'] == canon3['canonical_hash']:
print("\n✓ SUCCESS: All three triangles have IDENTICAL canonical forms!")
print(" This proves they are isomorphic.")
else:
print("\n✗ FAILED: Canonical forms differ (shouldn't happen for triangles)")
# Test with WL hash
print("\n" + "-"*70)
print("Method: Weisfeiler-Leman Hash (Fast Approximation)")
print("-"*70)
wl1 = canonicalizer.canonicalize(G1, method='wl_hash')
wl2 = canonicalizer.canonicalize(G2, method='wl_hash')
wl3 = canonicalizer.canonicalize(G3, method='wl_hash')
print(f"\nG1 WL hash: {wl1['canonical_hash'][:16]}...")
print(f"G2 WL hash: {wl2['canonical_hash'][:16]}...")
print(f"G3 WL hash: {wl3['canonical_hash'][:16]}...")
if wl1['canonical_hash'] == wl2['canonical_hash'] == wl3['canonical_hash']:
print("\n✓ WL hashes match (graphs likely isomorphic)")
else:
print("\n✗ WL hashes differ")
# Test with PyNauty if available
if canonicalizer.has_pynauty:
print("\n" + "-"*70)
print("Method: PyNauty (Industry Standard)")
print("-"*70)
nauty1 = canonicalizer.canonicalize(G1, method='pynauty')
nauty2 = canonicalizer.canonicalize(G2, method='pynauty')
nauty3 = canonicalizer.canonicalize(G3, method='pynauty')
print(f"\nG1 Nauty hash: {nauty1['canonical_hash'][:16]}...")
print(f"G2 Nauty hash: {nauty2['canonical_hash'][:16]}...")
print(f"G3 Nauty hash: {nauty3['canonical_hash'][:16]}...")
print(f"\nAutomorphism group sizes:")
print(f" G1: {nauty1['automorphism_group_size']}")
print(f" G2: {nauty2['automorphism_group_size']}")
print(f" G3: {nauty3['automorphism_group_size']}")
if nauty1['canonical_hash'] == nauty2['canonical_hash'] == nauty3['canonical_hash']:
print("\n✓ Nauty: All canonical forms match perfectly!")
# Demonstrate circuit library
print("\n" + "="*70)
print("CIRCUIT LIBRARY DEMONSTRATION")
print("="*70)
library = CanonicalCircuitLibrary()
# Add some circuits
print("\nBuilding circuit library...")
# NAND gate variations (all isomorphic)
nand1 = nx.Graph([('in1', 'gate'), ('in2', 'gate'), ('gate', 'out')])
nand2 = nx.Graph([('A', 'G'), ('B', 'G'), ('G', 'Z')])
nand3 = nx.Graph([('X', 'Y'), ('W', 'Y'), ('Y', 'Q')])
library.add_circuit('NAND_v1', nand1, {'type': 'logic_gate', 'function': 'NAND'})
library.add_circuit('NAND_v2', nand2, {'type': 'logic_gate', 'function': 'NAND'})
library.add_circuit('NAND_v3', nand3, {'type': 'logic_gate', 'function': 'NAND'})
# Different gate (NOT isomorphic)
nor = nx.Graph([('in1', 'gate'), ('in2', 'gate'), ('gate', 'out'), ('gate', 'gnd')])
library.add_circuit('NOR_v1', nor, {'type': 'logic_gate', 'function': 'NOR'})
print(f" Added 4 circuits to library")
print(f" Unique canonical forms: {len(library.library)}")
# Search for matches
print("\n" + "-"*70)
print("Searching for matches...")
print("-"*70)
query = nx.Graph([('p', 'q'), ('r', 'q'), ('q', 's')])
print(f"\nQuery circuit edges: {list(query.edges())}")
matches = library.find_matches(query)
print(f"\nFound {len(matches)} matching circuits:")
for match in matches:
print(f" - {match['circuit_id']}: {match['metadata']}")
print("\n✓ Canonical library allows O(1) lookup instead of comparing")
print(" against all circuits with VF2!")
if __name__ == "__main__":
demonstrate_canonicalization()
#!/usr/bin/python3.12
import pynauty
import networkx as nx
from matplotlib import pyplot as plt
def canonicalize_with_pynauty(G: nx.Graph) -> pynauty.Graph:
"""
Use PyNauty for true canonical form (if available).
This is the gold standard for canonicalization.
"""
# Convert NetworkX graph to pynauty format
# pynauty uses integer vertex labels starting from 0
node_to_int = {node: i for i, node in enumerate(G.nodes())}
n = len(node_to_int)
# Build adjacency dictionary for pynauty
adjacency_dict = {i: [] for i in range(n)}
for u, v in G.edges():
u_int = node_to_int[u]
v_int = node_to_int[v]
adjacency_dict[u_int].append(v_int)
adjacency_dict[v_int].append(u_int)
# Create pynauty graph
g = pynauty.Graph(number_of_vertices=n, directed=False, adjacency_dict=adjacency_dict)
certificate = pynauty.certificate(g).hex()
canonical_labeling=pynauty.canon_label(g)
canonical_graph = pynauty.Graph(n)
adjacency_dict=g._get_adjacency_dict()
for u in range(n):
for v in adjacency_dict[u]:
# Add edge in canonical graph if (u,v) is an edge in original graph
# and the canonical mapping ensures it's added only once (e.g., u < v)
if u < v: # Avoid adding duplicate edges in undirected graphs
canonical_graph.connect_vertex(canonical_labeling[u], canonical_labeling[v])
canonical_certificate = pynauty.certificate(canonical_graph).hex()
print("Canonization suceeded? ", canonical_certificate == certificate)
return g
ng=nx.Graph()
ng.add_nodes_from(["A","B", "C", "D", "E"])
ng.add_edges_from([("B","A"), ("B","C"), ("B","D"), ("B","E")])
g=canonicalize_with_pynauty(ng)
autogroup=pynauty.autgrp(g)
number_of_orbits = len(set(autogroup[3]))
print(f"Number of orbits = {number_of_orbits}")
print(f"Number of automorphisms = {int(autogroup[1])}")
pos = nx.spring_layout(ng)
nx.draw_networkx_nodes(ng, pos, node_color='lightblue', node_size=1500)
nx.draw_networkx_edges(ng, pos, width=3, alpha=0.7, edge_color='darkgreen')
nx.draw_networkx_labels(ng, pos, font_size=16, font_color='blue', font_weight="bold")
plt.title("NetworkX Graph", fontsize=16)
plt.axis('off') # Hide the axes
plt.gcf().canvas.manager.set_window_title('Graph Visualization Demo')
plt.show()
#!/usr/bin/python3.12
import networkx as nx
import matplotlib.pyplot as plt
# Example 1: Graph Isomorphism
# Check if two graphs are isomorphic (structurally identical)
print("=" * 50)
print("Example 1: Graph Isomorphism")
print("=" * 50)
# Create two graphs with same structure but different node labels
G1 = nx.Graph()
G1.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 0)])
G2 = nx.Graph()
G2.add_edges_from([('A', 'B'), ('B', 'C'), ('C', 'D'), ('D', 'A')])
# Check if graphs are isomorphic
GM = nx.isomorphism.GraphMatcher(G1, G2)
is_isomorphic = GM.is_isomorphic()
print(f"\nAre G1 and G2 isomorphic? {is_isomorphic}")
# Get the mapping between nodes
if is_isomorphic:
mapping = GM.mapping
print(f"Node mapping: {mapping}")
# Example 2: Subgraph Isomorphism
# Check if one graph is a subgraph of another
print("\n" + "=" * 50)
print("Example 2: Subgraph Isomorphism")
print("=" * 50)
# Create a larger graph
G_large = nx.Graph()
G_large.add_edges_from([
(1, 2), (2, 3), (3, 4), (4, 5),
(5, 1), (2, 6), (3, 7)
])
# Create a smaller pattern to find
G_pattern = nx.Graph()
G_pattern.add_edges_from([(1, 2), (2, 3), (3, 1)]) # Triangle
# Find subgraph isomorphism
GM = nx.isomorphism.GraphMatcher(G_large, G_pattern)
is_subgraph = GM.subgraph_is_isomorphic()
print(f"\nIs pattern a subgraph of large graph? {is_subgraph}")
# Get all subgraph matches
if is_subgraph:
print("\nAll matches found:")
for i, match in enumerate(GM.subgraph_isomorphisms_iter(), 1):
print(f" Match {i}: {match}")
# Example 3: Isomorphism with Node Attributes
print("\n" + "=" * 50)
print("Example 3: Isomorphism with Node Attributes")
print("=" * 50)
# Create graphs with node attributes
G3 = nx.Graph()
G3.add_node(1, color='red')
G3.add_node(2, color='blue')
G3.add_node(3, color='red')
G3.add_edges_from([(1, 2), (2, 3)])
G4 = nx.Graph()
G4.add_node('A', color='blue')
G4.add_node('B', color='red')
G4.add_node('C', color='red')
G4.add_edges_from([('A', 'B'), ('B', 'C')])
# Match with node attribute constraints
def node_match(n1, n2):
return n1['color'] == n2['color']
GM = nx.isomorphism.GraphMatcher(G3, G4, node_match=node_match)
is_iso_attr = GM.is_isomorphic()
print(f"\nAre graphs isomorphic with color constraint? {is_iso_attr}")
if is_iso_attr:
print(f"Mapping: {GM.mapping}")
# Example 4: Isomorphism with Edge Attributes
print("\n" + "=" * 50)
print("Example 4: Isomorphism with Edge Attributes")
print("=" * 50)
G5 = nx.Graph()
G5.add_edge(1, 2, weight=5)
G5.add_edge(2, 3, weight=10)
G6 = nx.Graph()
G6.add_edge('X', 'Y', weight=10)
G6.add_edge('Y', 'Z', weight=5)
def edge_match(e1, e2):
return e1['weight'] == e2['weight']
GM = nx.isomorphism.GraphMatcher(G5, G6, edge_match=edge_match)
is_iso_edge = GM.is_isomorphic()
print(f"\nAre graphs isomorphic with edge weight constraint? {is_iso_edge}")
if is_iso_edge:
print(f"Mapping: {GM.mapping}")
# Example 5: Directed Graph Isomorphism
print("\n" + "=" * 50)
print("Example 5: Directed Graph Isomorphism")
print("=" * 50)
DG1 = nx.DiGraph()
DG1.add_edges_from([(1, 2), (2, 3), (3, 1)])
DG2 = nx.DiGraph()
DG2.add_edges_from([('A', 'B'), ('B', 'C'), ('C', 'A')])
# Use DiGraphMatcher for directed graphs
DGM = nx.isomorphism.DiGraphMatcher(DG1, DG2)
is_di_iso = DGM.is_isomorphic()
print(f"\nAre directed graphs isomorphic? {is_di_iso}")
if is_di_iso:
print(f"Mapping: {DGM.mapping}")
# Visualization
print("\n" + "=" * 50)
print("Visualizing Example Graphs")
print("=" * 50)
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
# Plot G1 and G2
nx.draw(G1, ax=axes[0, 0], with_labels=True, node_color='lightblue',
node_size=500, font_size=16)
axes[0, 0].set_title('G1 (Square)')
nx.draw(G2, ax=axes[0, 1], with_labels=True, node_color='lightgreen',
node_size=500, font_size=16)
axes[0, 1].set_title('G2 (Square, isomorphic to G1)')
# Plot large graph and pattern
pos_large = nx.spring_layout(G_large)
nx.draw(G_large, pos_large, ax=axes[1, 0], with_labels=True,
node_color='lightcoral', node_size=500, font_size=16)
axes[1, 0].set_title('Large Graph')
nx.draw(G_pattern, ax=axes[1, 1], with_labels=True,
node_color='lightyellow', node_size=500, font_size=16)
axes[1, 1].set_title('Pattern (Triangle)')
plt.tight_layout()
plt.savefig('vf2_examples.png', dpi=150, bbox_inches='tight')
print("\nVisualization saved as 'vf2_examples.png'")
plt.show()
#!/usr/bin/python3.12
import networkx as nx
import matplotlib.pyplot as plt
print("=" * 60)
print("VF2 Subgraph Isomorphism - Finding Patterns in Graphs")
print("=" * 60)
# Example 1: Finding a Triangle Pattern
print("\nExample 1: Finding Triangle Patterns")
print("-" * 60)
# Create a larger graph (a social network)
G_social = nx.Graph()
G_social.add_edges_from([
(1, 2), (2, 3), (3, 1), # Triangle 1
(3, 4), (4, 5), (5, 6),
(6, 7), (7, 8), (8, 6), # Triangle 2
(4, 9), (9, 10)
])
# Pattern: A triangle (3 nodes forming a cycle)
pattern_triangle = nx.Graph()
pattern_triangle.add_edges_from([('A', 'B'), ('B', 'C'), ('C', 'A')])
# Find all occurrences of the triangle pattern
GM = nx.isomorphism.GraphMatcher(G_social, pattern_triangle)
print(f"Is triangle a subgraph? {GM.subgraph_is_isomorphic()}")
print("\nAll triangle patterns found:")
for i, match in enumerate(GM.subgraph_isomorphisms_iter(), 1):
print(f" Triangle {i}: {match}")
# Example 2: Finding a Path Pattern
print("\n" + "=" * 60)
print("Example 2: Finding Linear Path Patterns")
print("-" * 60)
# Pattern: A path of length 3 (4 nodes in a line)
pattern_path = nx.Graph()
pattern_path.add_edges_from([(1, 2), (2, 3), (3, 4)])
GM_path = nx.isomorphism.GraphMatcher(G_social, pattern_path)
print(f"\nIs path a subgraph? {GM_path.subgraph_is_isomorphic()}")
print("\nFirst 5 path patterns found:")
for i, match in enumerate(GM_path.subgraph_isomorphisms_iter(), 1):
if i > 5:
break
print(f" Path {i}: {match}")
# Example 3: Finding Star Pattern
print("\n" + "=" * 60)
print("Example 3: Finding Star Patterns (Hub and Spokes)")
print("-" * 60)
# Create a network with star patterns
G_network = nx.Graph()
G_network.add_edges_from([
# Star 1: node 1 is the hub
(1, 2), (1, 3), (1, 4),
# Additional connections
(2, 5), (3, 6),
# Star 2: node 7 is the hub
(7, 8), (7, 9), (7, 10),
(5, 7)
])
# Pattern: Star with 3 spokes
pattern_star = nx.Graph()
pattern_star.add_edges_from([('center', 'a'), ('center', 'b'), ('center', 'c')])
GM_star = nx.isomorphism.GraphMatcher(G_network, pattern_star)
print(f"\nIs star a subgraph? {GM_star.subgraph_is_isomorphic()}")
print("\nAll star patterns found:")
for i, match in enumerate(GM_star.subgraph_isomorphisms_iter(), 1):
print(f" Star {i}: {match}")
# Example 4: Chemical Structure - Finding Benzene Ring
print("\n" + "=" * 60)
print("Example 4: Chemical Structure - Finding Benzene Rings")
print("-" * 60)
# Larger molecule with multiple rings
G_molecule = nx.Graph()
# Benzene ring 1
G_molecule.add_edges_from([
(1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 1)
])
# Additional structure
G_molecule.add_edges_from([
(3, 7), (7, 8), (8, 9), (9, 10), (10, 11), (11, 8) # Another ring
])
# Pattern: Hexagonal ring (benzene-like)
pattern_hexagon = nx.Graph()
pattern_hexagon.add_edges_from([
('a', 'b'), ('b', 'c'), ('c', 'd'),
('d', 'e'), ('e', 'f'), ('f', 'a')
])
GM_chem = nx.isomorphism.GraphMatcher(G_molecule, pattern_hexagon)
print(f"\nIs hexagon a subgraph? {GM_chem.subgraph_is_isomorphic()}")
print("\nAll hexagonal rings found:")
for i, match in enumerate(GM_chem.subgraph_isomorphisms_iter(), 1):
print(f" Ring {i}: {match}")
# Example 5: Subgraph with Attributes
print("\n" + "=" * 60)
print("Example 5: Pattern Matching with Node Attributes")
print("-" * 60)
# Create a graph with colored nodes
G_colored = nx.Graph()
G_colored.add_node(1, color='red')
G_colored.add_node(2, color='blue')
G_colored.add_node(3, color='red')
G_colored.add_node(4, color='blue')
G_colored.add_node(5, color='red')
G_colored.add_node(6, color='green')
G_colored.add_edges_from([
(1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (1, 6)
])
# Pattern: red-blue-red sequence
pattern_colored = nx.Graph()
pattern_colored.add_node('X', color='red')
pattern_colored.add_node('Y', color='blue')
pattern_colored.add_node('Z', color='red')
pattern_colored.add_edges_from([('X', 'Y'), ('Y', 'Z')])
def node_match(n1, n2):
return n1['color'] == n2['color']
GM_colored = nx.isomorphism.GraphMatcher(G_colored, pattern_colored,
node_match=node_match)
print(f"\nIs colored pattern a subgraph? {GM_colored.subgraph_is_isomorphic()}")
print("\nAll colored patterns found:")
for i, match in enumerate(GM_colored.subgraph_isomorphisms_iter(), 1):
print(f" Pattern {i}: {match}")
# Visualization
print("\n" + "=" * 60)
print("Creating Visualizations...")
print("=" * 60)
fig = plt.figure(figsize=(16, 12))
# Example 1: Social Network with Triangles
ax1 = plt.subplot(3, 3, 1)
pos1 = nx.spring_layout(G_social, seed=42)
nx.draw(G_social, pos1, with_labels=True, node_color='lightblue',
node_size=600, font_size=12, font_weight='bold')
ax1.set_title('Social Network\n(Contains Triangles)', fontsize=12, fontweight='bold')
ax2 = plt.subplot(3, 3, 2)
pos2 = nx.circular_layout(pattern_triangle)
nx.draw(pattern_triangle, pos2, with_labels=True, node_color='yellow',
node_size=800, font_size=12, font_weight='bold')
ax2.set_title('Pattern: Triangle', fontsize=12, fontweight='bold')
# Highlight found triangles in the social network
ax3 = plt.subplot(3, 3, 3)
nx.draw(G_social, pos1, with_labels=True, node_color='lightgray',
node_size=600, font_size=12, alpha=0.3)
# Highlight first triangle found
first_triangle = list(GM.subgraph_isomorphisms_iter())[0]
triangle_nodes = list(first_triangle.keys())
nx.draw_networkx_nodes(G_social, pos1, nodelist=triangle_nodes,
node_color='red', node_size=600)
nx.draw_networkx_labels(G_social, pos1, font_size=12, font_weight='bold')
ax3.set_title('First Triangle Found\n(Highlighted)', fontsize=12, fontweight='bold')
# Example 2: Star Pattern
ax4 = plt.subplot(3, 3, 4)
pos3 = nx.spring_layout(G_network, seed=42)
nx.draw(G_network, pos3, with_labels=True, node_color='lightgreen',
node_size=600, font_size=12, font_weight='bold')
ax4.set_title('Network Graph\n(Contains Stars)', fontsize=12, fontweight='bold')
ax5 = plt.subplot(3, 3, 5)
pos4 = nx.spring_layout(pattern_star, seed=42)
nx.draw(pattern_star, pos4, with_labels=True, node_color='yellow',
node_size=800, font_size=12, font_weight='bold')
ax5.set_title('Pattern: Star (3 spokes)', fontsize=12, fontweight='bold')
# Highlight found star
ax6 = plt.subplot(3, 3, 6)
nx.draw(G_network, pos3, with_labels=True, node_color='lightgray',
node_size=600, font_size=12, alpha=0.3)
first_star = list(GM_star.subgraph_isomorphisms_iter())[0]
star_nodes = list(first_star.keys())
nx.draw_networkx_nodes(G_network, pos3, nodelist=star_nodes,
node_color='orange', node_size=600)
nx.draw_networkx_labels(G_network, pos3, font_size=12, font_weight='bold')
ax6.set_title('First Star Found\n(Highlighted)', fontsize=12, fontweight='bold')
# Example 3: Chemical Structure
ax7 = plt.subplot(3, 3, 7)
pos5 = nx.spring_layout(G_molecule, seed=42)
nx.draw(G_molecule, pos5, with_labels=True, node_color='lightcoral',
node_size=600, font_size=12, font_weight='bold')
ax7.set_title('Molecule\n(Contains Hexagonal Rings)', fontsize=12, fontweight='bold')
ax8 = plt.subplot(3, 3, 8)
pos6 = nx.circular_layout(pattern_hexagon)
nx.draw(pattern_hexagon, pos6, with_labels=True, node_color='yellow',
node_size=800, font_size=12, font_weight='bold')
ax8.set_title('Pattern: Hexagon', fontsize=12, fontweight='bold')
# Highlight found hexagon
ax9 = plt.subplot(3, 3, 9)
nx.draw(G_molecule, pos5, with_labels=True, node_color='lightgray',
node_size=600, font_size=12, alpha=0.3)
first_hex = list(GM_chem.subgraph_isomorphisms_iter())[0]
hex_nodes = list(first_hex.keys())
nx.draw_networkx_nodes(G_molecule, pos5, nodelist=hex_nodes,
node_color='purple', node_size=600)
nx.draw_networkx_labels(G_molecule, pos5, font_size=12, font_weight='bold')
ax9.set_title('First Hexagon Found\n(Highlighted)', fontsize=12, fontweight='bold')
plt.tight_layout()
plt.savefig('vf2_subgraph_examples.png', dpi=150, bbox_inches='tight')
print("\nVisualization saved as 'vf2_subgraph_examples.png'")
plt.show()
print("\n" + "=" * 60)
print("Summary: VF2 successfully found patterns as subgraphs!")
print("=" * 60)
#!/usr/bin/python3.12
"""
Graph Isomorphism Detection Pipeline
Combines Weisfeiler-Leman (fast filtering) with VF2 (exact matching)
for efficient circuit topology comparison
"""
import networkx as nx
from collections import defaultdict, Counter
import time
from typing import List, Tuple, Optional, Dict, Set
class WeisfeilerLeman:
"""
Weisfeiler-Leman algorithm for graph isomorphism testing.
Provides fast polynomial-time filtering but is incomplete.
"""
def __init__(self, max_iterations: int = 100):
self.max_iterations = max_iterations
def compute_color_signature(self, G: nx.Graph) -> Tuple[tuple, List[Dict]]:
"""
Compute WL color signature for a graph.
Returns: (color_distribution, iteration_history)
"""
# Initialize: color each vertex by its degree
colors = {node: G.degree(node) for node in G.nodes()}
history = [colors.copy()]
for iteration in range(self.max_iterations):
new_colors = {}
for node in G.nodes():
# Collect neighbor colors
neighbor_colors = sorted([colors[neighbor] for neighbor in G.neighbors(node)])
# Create signature: (my_color, sorted_neighbor_colors)
signature = (colors[node], tuple(neighbor_colors))
# Hash the signature to create new color
new_colors[node] = hash(signature)
# Check if colors stabilized
if new_colors == colors:
break
colors = new_colors
history.append(colors.copy())
# Return color distribution (sorted counts)
color_counts = tuple(sorted(Counter(colors.values()).items()))
return color_counts, history
def test_isomorphism(self, G1: nx.Graph, G2: nx.Graph) -> Tuple[bool, str]:
"""
Test if two graphs might be isomorphic.
Returns: (possibly_isomorphic, reason)
"""
# FAST CHECK 1: Node count (O(1))
if G1.number_of_nodes() != G2.number_of_nodes():
return False, "Different number of nodes"
# FAST CHECK 2: Edge count (O(1))
if G1.number_of_edges() != G2.number_of_edges():
return False, "Different number of edges"
# FAST CHECK 3: Degree sequence (O(n log n))
# This is much faster than full WL and catches most non-isomorphic graphs
deg_seq1 = sorted([d for n, d in G1.degree()])
deg_seq2 = sorted([d for n, d in G2.degree()])
if deg_seq1 != deg_seq2:
return False, "Different degree sequences"
# FAST CHECK 4: Triangle count (O(n*d^2) but fast in practice)
triangles1 = sum(nx.triangles(G1).values()) // 3
triangles2 = sum(nx.triangles(G2).values()) // 3
if triangles1 != triangles2:
return False, "Different triangle counts"
# Only run expensive WL if all fast checks passed
# WL color refinement (O(n log n) but with higher constant factor)
sig1, history1 = self.compute_color_signature(G1)
sig2, history2 = self.compute_color_signature(G2)
if sig1 != sig2:
return False, f"Different WL signatures after {len(history1)} iterations"
return True, f"WL test passed (signatures match after {len(history1)} iterations)"
class VF2Matcher:
"""
VF2 algorithm for complete graph isomorphism.
Provides definitive answer but can be slow on large/symmetric graphs.
"""
def __init__(self):
self.call_count = 0
def find_isomorphism(self, G1: nx.Graph, G2: nx.Graph) -> Tuple[bool, Optional[Dict], int]:
"""
Find an isomorphism between two graphs using VF2.
Returns: (is_isomorphic, mapping, num_recursive_calls)
"""
self.call_count = 0
# Use NetworkX's built-in VF2 implementation
matcher = nx.algorithms.isomorphism.GraphMatcher(G1, G2)
try:
is_iso = matcher.is_isomorphic()
mapping = matcher.mapping if is_iso else None
return is_iso, mapping, self.call_count
except:
return False, None, self.call_count
def custom_vf2(self, G1: nx.Graph, G2: nx.Graph) -> Tuple[bool, Optional[Dict]]:
"""
Simplified custom VF2 implementation for educational purposes.
"""
self.call_count = 0
def is_feasible(mapping: Dict, n1, n2) -> bool:
"""Check if adding (n1, n2) to mapping is feasible."""
# Check if already mapped
if n1 in mapping or n2 in mapping.values():
return False
# Semantic feasibility: degrees must match
if G1.degree(n1) != G2.degree(n2):
return False
# Syntactic feasibility: mapped neighbors must correspond
for neighbor1 in G1.neighbors(n1):
if neighbor1 in mapping:
neighbor2 = mapping[neighbor1]
if neighbor2 not in G2.neighbors(n2):
return False
return True
def backtrack(mapping: Dict) -> bool:
"""Recursive backtracking search."""
self.call_count += 1
# Base case: all vertices mapped
if len(mapping) == G1.number_of_nodes():
return True
# Choose next unmapped vertex from G1
unmapped1 = [n for n in G1.nodes() if n not in mapping]
if not unmapped1:
return False
n1 = unmapped1[0]
# Try mapping to each unmapped vertex in G2
unmapped2 = [n for n in G2.nodes() if n not in mapping.values()]
for n2 in unmapped2:
if is_feasible(mapping, n1, n2):
# Add to mapping
mapping[n1] = n2
# Recurse
if backtrack(mapping):
return True
# Backtrack
del mapping[n1]
return False
mapping = {}
found = backtrack(mapping)
return found, mapping if found else None
class IsomorphismPipeline:
"""
Combined pipeline: Fast pre-checks + WL filtering + Exact VF2 matching
"""
def __init__(self):
self.wl = WeisfeilerLeman()
self.vf2 = VF2Matcher()
self.stats = {
'basic_rejections': 0, # Rejected by node/edge count
'degree_rejections': 0, # Rejected by degree sequence
'triangle_rejections': 0, # Rejected by triangle count
'wl_rejections': 0, # Rejected by WL
'wl_passes': 0, # Passed WL, need VF2
'vf2_confirmations': 0,
'vf2_rejections': 0
}
self.timing = {
'basic_time': 0,
'degree_time': 0,
'triangle_time': 0,
'wl_time': 0,
'vf2_time': 0
}
def test_isomorphism(self, G1: nx.Graph, G2: nx.Graph,
use_vf2: bool = True) -> Dict:
"""
Test isomorphism using multi-tier approach with detailed timing.
Args:
G1, G2: Graphs to compare
use_vf2: If True, run VF2 when all filters pass
Returns:
Dictionary with results and timing breakdown
"""
result = {
'isomorphic': None,
'passed_basic': False,
'passed_degree': False,
'passed_triangle': False,
'passed_wl': False,
'vf2_used': False,
'basic_time': 0,
'degree_time': 0,
'triangle_time': 0,
'wl_time': 0,
'vf2_time': 0,
'total_time': 0,
'mapping': None,
'reason': ''
}
start_time = time.time()
# Stage 1: Basic checks (node/edge count) - O(1)
t = time.time()
if G1.number_of_nodes() != G2.number_of_nodes():
result['basic_time'] = time.time() - t
result['reason'] = "Basic check: Different number of nodes"
result['isomorphic'] = False
self.stats['basic_rejections'] += 1
self.timing['basic_time'] += result['basic_time']
result['total_time'] = time.time() - start_time
return result
if G1.number_of_edges() != G2.number_of_edges():
result['basic_time'] = time.time() - t
result['reason'] = "Basic check: Different number of edges"
result['isomorphic'] = False
self.stats['basic_rejections'] += 1
self.timing['basic_time'] += result['basic_time']
result['total_time'] = time.time() - start_time
return result
result['basic_time'] = time.time() - t
result['passed_basic'] = True
self.timing['basic_time'] += result['basic_time']
# Stage 2: Degree sequence check - O(n log n)
t = time.time()
deg_seq1 = sorted([d for n, d in G1.degree()])
deg_seq2 = sorted([d for n, d in G2.degree()])
result['degree_time'] = time.time() - t
self.timing['degree_time'] += result['degree_time']
if deg_seq1 != deg_seq2:
result['reason'] = "Degree sequence check: Different degree distributions"
result['isomorphic'] = False
self.stats['degree_rejections'] += 1
result['total_time'] = time.time() - start_time
return result
result['passed_degree'] = True
# Stage 3: Triangle count - O(n*d^2), fast for sparse graphs
t = time.time()
triangles1 = sum(nx.triangles(G1).values()) // 3
triangles2 = sum(nx.triangles(G2).values()) // 3
result['triangle_time'] = time.time() - t
self.timing['triangle_time'] += result['triangle_time']
if triangles1 != triangles2:
result['reason'] = "Triangle count check: Different triangle counts"
result['isomorphic'] = False
self.stats['triangle_rejections'] += 1
result['total_time'] = time.time() - start_time
return result
result['passed_triangle'] = True
# Stage 4: WL color refinement - O(n log n) but higher constant
t = time.time()
sig1, history1 = self.wl.compute_color_signature(G1)
sig2, history2 = self.wl.compute_color_signature(G2)
result['wl_time'] = time.time() - t
self.timing['wl_time'] += result['wl_time']
if sig1 != sig2:
result['reason'] = f"WL check: Different color signatures after {len(history1)} iterations"
result['isomorphic'] = False
self.stats['wl_rejections'] += 1
result['total_time'] = time.time() - start_time
return result
result['passed_wl'] = True
result['reason'] = f"All filters passed (WL: {len(history1)} iterations)"
self.stats['wl_passes'] += 1
# Stage 5: VF2 exact matching (if requested)
if use_vf2:
t = time.time()
is_iso, mapping, calls = self.vf2.find_isomorphism(G1, G2)
result['vf2_time'] = time.time() - t
result['vf2_used'] = True
result['isomorphic'] = is_iso
result['mapping'] = mapping
self.timing['vf2_time'] += result['vf2_time']
if is_iso:
result['reason'] += " → VF2 confirmed isomorphism"
self.stats['vf2_confirmations'] += 1
else:
result['reason'] += " → VF2 rejected (false positive)"
self.stats['vf2_rejections'] += 1
else:
result['isomorphic'] = None # Unknown without VF2
result['total_time'] = time.time() - start_time
return result
def print_stats(self):
"""Print pipeline statistics with detailed breakdown."""
print("\n" + "="*60)
print("PIPELINE STATISTICS")
print("="*60)
total = sum(self.stats.values())
print(f"Total comparisons: {total}")
print(f"\nREJECTIONS BY STAGE:")
print(f" Basic checks (nodes/edges): {self.stats['basic_rejections']:>4} ({self.stats['basic_rejections']/total*100:>5.1f}%)")
print(f" Degree sequence: {self.stats['degree_rejections']:>4} ({self.stats['degree_rejections']/total*100:>5.1f}%)")
print(f" Triangle count: {self.stats['triangle_rejections']:>4} ({self.stats['triangle_rejections']/total*100:>5.1f}%)")
print(f" WL color refinement: {self.stats['wl_rejections']:>4} ({self.stats['wl_rejections']/total*100:>5.1f}%)")
print(f" Passed all filters (VF2 ran): {self.stats['wl_passes']:>4} ({self.stats['wl_passes']/total*100:>5.1f}%)")
if self.stats['vf2_confirmations'] > 0 or self.stats['vf2_rejections'] > 0:
print(f"\nVF2 RESULTS:")
print(f" Confirmed isomorphic: {self.stats['vf2_confirmations']:>4}")
print(f" Rejected (false positives): {self.stats['vf2_rejections']:>4}")
fast_rejected = (self.stats['basic_rejections'] +
self.stats['degree_rejections'] +
self.stats['triangle_rejections'])
print(f"\nFILTERING EFFICIENCY:")
print(f" Fast rejected (no WL/VF2): {fast_rejected:>4} ({fast_rejected/total*100:>5.1f}%)")
print(f" Required expensive checks: {self.stats['wl_rejections'] + self.stats['wl_passes']:>4} ({(self.stats['wl_rejections'] + self.stats['wl_passes'])/total*100:>5.1f}%)")
print(f"\nCUMULATIVE TIMING:")
print(f" Basic checks: {self.timing['basic_time']*1000:>8.2f} ms")
print(f" Degree sequence: {self.timing['degree_time']*1000:>8.2f} ms")
print(f" Triangle count: {self.timing['triangle_time']*1000:>8.2f} ms")
print(f" WL refinement: {self.timing['wl_time']*1000:>8.2f} ms")
print(f" VF2 matching: {self.timing['vf2_time']*1000:>8.2f} ms")
total_filter_time = (self.timing['basic_time'] + self.timing['degree_time'] +
self.timing['triangle_time'] + self.timing['wl_time'])
print(f" Total filtering: {total_filter_time*1000:>8.2f} ms")
print(f" Total VF2: {self.timing['vf2_time']*1000:>8.2f} ms")
def create_example_graphs() -> List[Tuple[nx.Graph, nx.Graph, str]]:
"""Create example graph pairs for testing - scaled to show filter advantage."""
examples = []
# Example 1: LARGE dense graph (VF2 struggles here)
import random
random.seed(42)
G1 = nx.gnm_random_graph(150, 1500, seed=42) # Much larger and denser
G2 = nx.gnm_random_graph(150, 1500, seed=43)
examples.append((G1, G2, "Large dense graphs (150 nodes, 1500 edges) - filters win"))
# Example 2: Regular graphs (highly symmetric - VF2's worst case)
G3 = nx.random_regular_graph(4, 100, seed=42) # All nodes degree 4
G4 = nx.random_regular_graph(4, 100, seed=43) # Different instance
examples.append((G3, G4, "Regular graphs (100 nodes, all degree 4) - VF2 struggles"))
# Example 3: Large scale-free graphs (realistic for circuits)
G5 = nx.barabasi_albert_graph(200, 5, seed=100)
G6 = nx.barabasi_albert_graph(200, 5, seed=101)
examples.append((G5, G6, "Scale-free graphs (200 nodes) - filters fast, VF2 slow"))
# Example 4: Grid graph variants (VF2's exponential behavior shows)
G7 = nx.grid_2d_graph(12, 12) # 144 nodes, highly symmetric
G8 = nx.grid_2d_graph(12, 12)
# Make them different by rewiring a few edges
edges = list(G8.edges())
G8.remove_edge(*edges[0])
G8.add_edge(edges[0][0], edges[5][1])
examples.append((G7, G8, "Large grid graphs (144 nodes) - VF2 explores many symmetries"))
# Example 5: Complete bipartite (maximum symmetry)
G9 = nx.complete_bipartite_graph(40, 40) # 80 nodes, highly symmetric
G10 = nx.complete_bipartite_graph(40, 41) # Different size
examples.append((G9, G10, "Complete bipartite - basic filter catches instantly"))
return examples
def main():
"""Demonstrate the two-tier isomorphism detection pipeline."""
print("="*60)
print("GRAPH ISOMORPHISM DETECTION PIPELINE")
print("Weisfeiler-Leman (fast filtering) + VF2 (exact matching)")
print("="*60)
pipeline = IsomorphismPipeline()
examples = create_example_graphs()
for i, (G1, G2, description) in enumerate(examples, 1):
print(f"\n{'='*60}")
print(f"Example {i}: {description}")
print(f"{'='*60}")
print(f"G1: {G1.number_of_nodes()} nodes, {G1.number_of_edges()} edges")
print(f"G2: {G2.number_of_nodes()} nodes, {G2.number_of_edges()} edges")
# Run pipeline
result = pipeline.test_isomorphism(G1, G2, use_vf2=True)
# Print results
print(f"\n{'─'*60}")
print(f"Result: {'ISOMORPHIC' if result['isomorphic'] else 'NOT ISOMORPHIC'}")
print(f"{'─'*60}")
print(f"Reason: {result['reason']}")
print(f"\nTiming breakdown:")
print(f" Basic checks: {result['basic_time']*1000:>6.3f} ms")
if result['passed_basic']:
print(f" Degree sequence: {result['degree_time']*1000:>6.3f} ms")
if result['passed_degree']:
print(f" Triangle count: {result['triangle_time']*1000:>6.3f} ms")
if result['passed_triangle']:
print(f" WL refinement: {result['wl_time']*1000:>6.3f} ms")
if result['vf2_used']:
print(f" VF2 matching: {result['vf2_time']*1000:>6.3f} ms")
print(f" Total: {result['total_time']*1000:>6.3f} ms")
if result['mapping'] and result['isomorphic']:
print(f"\nMapping (first 5 pairs): {dict(list(result['mapping'].items())[:5])}")
print("\n" + "="*60)
print("Note: Small examples above show individual algorithm behavior.")
print("See efficiency demo below for when filters actually win.")
print("="*60)
# Statistics already printed in the efficiency demo above
# Demonstrate efficiency on larger dataset
print("\n" + "="*60)
print("REAL-WORLD SCENARIO: When filters actually WIN")
print("="*60)
print("Scenario: Pre-screening 10,000 library cells with OBVIOUS mismatches")
print("Key insight: Don't run expensive checks when cheap ones suffice!")
# Create a reference graph - moderate size
reference = nx.barabasi_albert_graph(80, 3, seed=42)
print(f"\nReference circuit: {reference.number_of_nodes()} nodes, {reference.number_of_edges()} edges")
print("\nGenerating library of 10,000 diverse circuits...")
print(" - 100 isomorphic (exact relabeling)")
print(" - 3,000 wrong size (different node count) → O(1) rejection")
print(" - 2,000 wrong density (different edge count) → O(1) rejection")
print(" - 3,000 wrong degree distribution → O(n) rejection")
print(" - 1,900 structurally different → need VF2")
# Strategy comparison
import random
random.seed(42)
print("\nRunning comparisons...")
# Strategy 1: VF2 only
print(f"\n Testing: VF2 only...")
vf2_only_time = 0
random.seed(42) # Reset for identical graphs
for i in range(10000):
# Generate test graphs with known properties
if i < 100:
# Isomorphic (relabeled)
nodes = list(reference.nodes())
random.shuffle(nodes)
mapping = {old: new for old, new in zip(reference.nodes(), nodes)}
test_graph = nx.relabel_nodes(reference, mapping)
elif i < 3100:
# WRONG SIZE - filters catch instantly, VF2 still needs to check
test_graph = nx.barabasi_albert_graph(random.choice([60, 70, 90, 100]), 3, seed=i)
elif i < 5100:
# WRONG EDGE COUNT - filters catch instantly
test_graph = nx.gnm_random_graph(80, random.choice([150, 180, 280, 320]), seed=i)
elif i < 8100:
# WRONG DEGREE SEQUENCE - filters catch with O(n) check
test_graph = nx.random_regular_graph(random.choice([3, 4, 5, 6]), 80, seed=i)
else:
# Actually need to check structure (similar stats)
test_graph = nx.barabasi_albert_graph(80, 3, seed=i)
# VF2 only - no filtering
start = time.time()
matcher = nx.algorithms.isomorphism.GraphMatcher(reference, test_graph)
_ = matcher.is_isomorphic()
vf2_only_time += time.time() - start
if (i + 1) % 2000 == 0:
print(f" {i+1}/10,000 completed...")
# Strategy 2: With filters
print(f"\n Testing: With filters...")
filter_pipeline = IsomorphismPipeline()
filter_time = 0
random.seed(42) # Reset for identical graphs
for i in range(10000):
# Generate same test graphs
if i < 100:
nodes = list(reference.nodes())
random.shuffle(nodes)
mapping = {old: new for old, new in zip(reference.nodes(), nodes)}
test_graph = nx.relabel_nodes(reference, mapping)
elif i < 3100:
test_graph = nx.barabasi_albert_graph(random.choice([60, 70, 90, 100]), 3, seed=i)
elif i < 5100:
test_graph = nx.gnm_random_graph(80, random.choice([150, 180, 280, 320]), seed=i)
elif i < 8100:
test_graph = nx.random_regular_graph(random.choice([3, 4, 5, 6]), 80, seed=i)
else:
test_graph = nx.barabasi_albert_graph(80, 3, seed=i)
# Use filtering pipeline
result = filter_pipeline.test_isomorphism(reference, test_graph, use_vf2=True)
filter_time += result['total_time']
if (i + 1) % 2000 == 0:
print(f" {i+1}/10,000 completed...")
# Report results
print("\n" + "="*60)
print("FINAL RESULTS")
print("="*60)
print(f"\nVF2 only (no filtering): {vf2_only_time*1000:>10.2f} ms ({vf2_only_time:.3f}s)")
print(f"With filtering pipeline: {filter_time*1000:>10.2f} ms ({filter_time:.3f}s)")
print(f"\n{'='*60}")
if filter_time < vf2_only_time:
speedup = vf2_only_time / filter_time
print(f"✓ SUCCESS: {speedup:.2f}x FASTER with filtering!")
print(f" Time saved: {(vf2_only_time - filter_time)*1000:.2f} ms ({(vf2_only_time - filter_time):.3f}s)")
else:
slowdown = filter_time / vf2_only_time
print(f"✗ FAILURE: {slowdown:.2f}x slower with filtering")
print(f" Time wasted: {(filter_time - vf2_only_time)*1000:.2f} ms")
print(f"{'='*60}")
# Show WHY it works (using filter_pipeline stats)
filter_pipeline.print_stats()
print(f"\n{'='*60}")
print("WHY FILTERS WIN:")
print(f"{'='*60}")
cheap_rejections = filter_pipeline.stats['basic_rejections'] + filter_pipeline.stats['degree_rejections']
print(f"Cheap O(1) and O(n) checks rejected: {cheap_rejections} graphs ({cheap_rejections/100:.1f}%)")
print(f"These would have taken ~{cheap_rejections * (vf2_only_time/10000):.3f}s with VF2")
print(f"But took only ~{(filter_pipeline.timing['basic_time'] + filter_pipeline.timing['degree_time']):.3f}s with filters")
print(f"Savings: {cheap_rejections * (vf2_only_time/10000) - (filter_pipeline.timing['basic_time'] + filter_pipeline.timing['degree_time']):.3f}s")
print(f"\nPer-comparison averages:")
print(f" VF2 only: {vf2_only_time/10000*1000:.4f} ms per graph")
print(f" With filters: {filter_time/10000*1000:.4f} ms per graph")
# The key insight
print(f"\n{'='*60}")
print("KEY INSIGHT FOR YOUR INTERVIEW:")
print("="*60)
print("Filters win when the dataset has:")
print(" 1. Many OBVIOUS mismatches (wrong size, density)")
print(" 2. Large scale (10,000+ comparisons)")
print(" 3. Cheap O(1) checks that eliminate bulk of candidates")
print("")
print("VF2-only wins when:")
print(" 1. Most graphs need deep structural analysis anyway")
print(" 2. Small datasets (< 1000 comparisons)")
print(" 3. Graphs are similar in basic properties")
print("")
print("In semiconductor EDA:")
print(" - Use filters for initial library screening")
print(" - Use VF2 directly for refined candidate comparison")
print(" - Architecture matters more than algorithm choice")
print("="*60)
if __name__ == "__main__":
main()
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment