321 lines
10 KiB
Python
321 lines
10 KiB
Python
|
"""
|
||
|
Tests for VF2 isomorphism algorithm.
|
||
|
"""
|
||
|
|
||
|
import os
|
||
|
import struct
|
||
|
import random
|
||
|
|
||
|
import pytest
|
||
|
import networkx as nx
|
||
|
from networkx.algorithms import isomorphism as iso
|
||
|
|
||
|
|
||
|
class TestWikipediaExample(object):
|
||
|
# Source: https://en.wikipedia.org/wiki/Graph_isomorphism
|
||
|
|
||
|
# Nodes 'a', 'b', 'c' and 'd' form a column.
|
||
|
# Nodes 'g', 'h', 'i' and 'j' form a column.
|
||
|
g1edges = [['a', 'g'], ['a', 'h'], ['a', 'i'],
|
||
|
['b', 'g'], ['b', 'h'], ['b', 'j'],
|
||
|
['c', 'g'], ['c', 'i'], ['c', 'j'],
|
||
|
['d', 'h'], ['d', 'i'], ['d', 'j']]
|
||
|
|
||
|
# Nodes 1,2,3,4 form the clockwise corners of a large square.
|
||
|
# Nodes 5,6,7,8 form the clockwise corners of a small square
|
||
|
g2edges = [[1, 2], [2, 3], [3, 4], [4, 1],
|
||
|
[5, 6], [6, 7], [7, 8], [8, 5],
|
||
|
[1, 5], [2, 6], [3, 7], [4, 8]]
|
||
|
|
||
|
def test_graph(self):
|
||
|
g1 = nx.Graph()
|
||
|
g2 = nx.Graph()
|
||
|
g1.add_edges_from(self.g1edges)
|
||
|
g2.add_edges_from(self.g2edges)
|
||
|
gm = iso.GraphMatcher(g1, g2)
|
||
|
assert gm.is_isomorphic()
|
||
|
#Just testing some cases
|
||
|
assert gm.subgraph_is_monomorphic()
|
||
|
|
||
|
mapping = sorted(gm.mapping.items())
|
||
|
# this mapping is only one of the possibilies
|
||
|
# so this test needs to be reconsidered
|
||
|
# isomap = [('a', 1), ('b', 6), ('c', 3), ('d', 8),
|
||
|
# ('g', 2), ('h', 5), ('i', 4), ('j', 7)]
|
||
|
# assert_equal(mapping, isomap)
|
||
|
|
||
|
def test_subgraph(self):
|
||
|
g1 = nx.Graph()
|
||
|
g2 = nx.Graph()
|
||
|
g1.add_edges_from(self.g1edges)
|
||
|
g2.add_edges_from(self.g2edges)
|
||
|
g3 = g2.subgraph([1, 2, 3, 4])
|
||
|
gm = iso.GraphMatcher(g1, g3)
|
||
|
assert gm.subgraph_is_isomorphic()
|
||
|
|
||
|
|
||
|
def test_subgraph_mono(self):
|
||
|
g1 = nx.Graph()
|
||
|
g2 = nx.Graph()
|
||
|
g1.add_edges_from(self.g1edges)
|
||
|
g2.add_edges_from([[1, 2], [2, 3], [3, 4]])
|
||
|
gm = iso.GraphMatcher(g1, g2)
|
||
|
assert gm.subgraph_is_monomorphic()
|
||
|
|
||
|
|
||
|
class TestVF2GraphDB(object):
|
||
|
# http://amalfi.dis.unina.it/graph/db/
|
||
|
|
||
|
@staticmethod
|
||
|
def create_graph(filename):
|
||
|
"""Creates a Graph instance from the filename."""
|
||
|
|
||
|
# The file is assumed to be in the format from the VF2 graph database.
|
||
|
# Each file is composed of 16-bit numbers (unsigned short int).
|
||
|
# So we will want to read 2 bytes at a time.
|
||
|
|
||
|
# We can read the number as follows:
|
||
|
# number = struct.unpack('<H', file.read(2))
|
||
|
# This says, expect the data in little-endian encoding
|
||
|
# as an unsigned short int and unpack 2 bytes from the file.
|
||
|
|
||
|
fh = open(filename, mode='rb')
|
||
|
|
||
|
# Grab the number of nodes.
|
||
|
# Node numeration is 0-based, so the first node has index 0.
|
||
|
nodes = struct.unpack('<H', fh.read(2))[0]
|
||
|
|
||
|
graph = nx.Graph()
|
||
|
for from_node in range(nodes):
|
||
|
# Get the number of edges.
|
||
|
edges = struct.unpack('<H', fh.read(2))[0]
|
||
|
for edge in range(edges):
|
||
|
# Get the terminal node.
|
||
|
to_node = struct.unpack('<H', fh.read(2))[0]
|
||
|
graph.add_edge(from_node, to_node)
|
||
|
|
||
|
fh.close()
|
||
|
return graph
|
||
|
|
||
|
def test_graph(self):
|
||
|
head, tail = os.path.split(__file__)
|
||
|
g1 = self.create_graph(os.path.join(head, 'iso_r01_s80.A99'))
|
||
|
g2 = self.create_graph(os.path.join(head, 'iso_r01_s80.B99'))
|
||
|
gm = iso.GraphMatcher(g1, g2)
|
||
|
assert gm.is_isomorphic()
|
||
|
|
||
|
def test_subgraph(self):
|
||
|
# A is the subgraph
|
||
|
# B is the full graph
|
||
|
head, tail = os.path.split(__file__)
|
||
|
subgraph = self.create_graph(os.path.join(head, 'si2_b06_m200.A99'))
|
||
|
graph = self.create_graph(os.path.join(head, 'si2_b06_m200.B99'))
|
||
|
gm = iso.GraphMatcher(graph, subgraph)
|
||
|
assert gm.subgraph_is_isomorphic()
|
||
|
#Just testing some cases
|
||
|
assert gm.subgraph_is_monomorphic()
|
||
|
|
||
|
# There isn't a similar test implemented for subgraph monomorphism,
|
||
|
# feel free to create one.
|
||
|
|
||
|
|
||
|
class TestAtlas(object):
|
||
|
@classmethod
|
||
|
def setup_class(cls):
|
||
|
global atlas
|
||
|
import platform
|
||
|
if platform.python_implementation() == 'Jython':
|
||
|
pytest.mark.skip('graph atlas not available under Jython.')
|
||
|
import networkx.generators.atlas as atlas
|
||
|
|
||
|
cls.GAG = atlas.graph_atlas_g()
|
||
|
|
||
|
def test_graph_atlas(self):
|
||
|
# Atlas = nx.graph_atlas_g()[0:208] # 208, 6 nodes or less
|
||
|
Atlas = self.GAG[0:100]
|
||
|
alphabet = list(range(26))
|
||
|
for graph in Atlas:
|
||
|
nlist = list(graph)
|
||
|
labels = alphabet[:len(nlist)]
|
||
|
for s in range(10):
|
||
|
random.shuffle(labels)
|
||
|
d = dict(zip(nlist, labels))
|
||
|
relabel = nx.relabel_nodes(graph, d)
|
||
|
gm = iso.GraphMatcher(graph, relabel)
|
||
|
assert gm.is_isomorphic()
|
||
|
|
||
|
|
||
|
def test_multiedge():
|
||
|
# Simple test for multigraphs
|
||
|
# Need something much more rigorous
|
||
|
edges = [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5),
|
||
|
(5, 6), (6, 7), (7, 8), (8, 9), (9, 10),
|
||
|
(10, 11), (10, 11), (11, 12), (11, 12),
|
||
|
(12, 13), (12, 13), (13, 14), (13, 14),
|
||
|
(14, 15), (14, 15), (15, 16), (15, 16),
|
||
|
(16, 17), (16, 17), (17, 18), (17, 18),
|
||
|
(18, 19), (18, 19), (19, 0), (19, 0)]
|
||
|
nodes = list(range(20))
|
||
|
|
||
|
for g1 in [nx.MultiGraph(), nx.MultiDiGraph()]:
|
||
|
g1.add_edges_from(edges)
|
||
|
for _ in range(10):
|
||
|
new_nodes = list(nodes)
|
||
|
random.shuffle(new_nodes)
|
||
|
d = dict(zip(nodes, new_nodes))
|
||
|
g2 = nx.relabel_nodes(g1, d)
|
||
|
if not g1.is_directed():
|
||
|
gm = iso.GraphMatcher(g1, g2)
|
||
|
else:
|
||
|
gm = iso.DiGraphMatcher(g1, g2)
|
||
|
assert gm.is_isomorphic()
|
||
|
#Testing if monomorphism works in multigraphs
|
||
|
assert gm.subgraph_is_monomorphic()
|
||
|
|
||
|
|
||
|
def test_selfloop():
|
||
|
# Simple test for graphs with selfloops
|
||
|
edges = [(0, 1), (0, 2), (1, 2), (1, 3), (2, 2),
|
||
|
(2, 4), (3, 1), (3, 2), (4, 2), (4, 5), (5, 4)]
|
||
|
nodes = list(range(6))
|
||
|
|
||
|
for g1 in [nx.Graph(), nx.DiGraph()]:
|
||
|
g1.add_edges_from(edges)
|
||
|
for _ in range(100):
|
||
|
new_nodes = list(nodes)
|
||
|
random.shuffle(new_nodes)
|
||
|
d = dict(zip(nodes, new_nodes))
|
||
|
g2 = nx.relabel_nodes(g1, d)
|
||
|
if not g1.is_directed():
|
||
|
gm = iso.GraphMatcher(g1, g2)
|
||
|
else:
|
||
|
gm = iso.DiGraphMatcher(g1, g2)
|
||
|
assert gm.is_isomorphic()
|
||
|
|
||
|
|
||
|
def test_selfloop_mono():
|
||
|
# Simple test for graphs with selfloops
|
||
|
edges0 = [(0, 1), (0, 2), (1, 2), (1, 3),
|
||
|
(2, 4), (3, 1), (3, 2), (4, 2), (4, 5), (5, 4)]
|
||
|
edges = edges0 + [(2, 2)]
|
||
|
nodes = list(range(6))
|
||
|
|
||
|
for g1 in [nx.Graph(), nx.DiGraph()]:
|
||
|
g1.add_edges_from(edges)
|
||
|
for _ in range(100):
|
||
|
new_nodes = list(nodes)
|
||
|
random.shuffle(new_nodes)
|
||
|
d = dict(zip(nodes, new_nodes))
|
||
|
g2 = nx.relabel_nodes(g1, d)
|
||
|
g2.remove_edges_from(nx.selfloop_edges(g2))
|
||
|
if not g1.is_directed():
|
||
|
gm = iso.GraphMatcher(g2, g1)
|
||
|
else:
|
||
|
gm = iso.DiGraphMatcher(g2, g1)
|
||
|
assert not gm.subgraph_is_monomorphic()
|
||
|
|
||
|
|
||
|
def test_isomorphism_iter1():
|
||
|
# As described in:
|
||
|
# http://groups.google.com/group/networkx-discuss/browse_thread/thread/2ff65c67f5e3b99f/d674544ebea359bb?fwc=1
|
||
|
g1 = nx.DiGraph()
|
||
|
g2 = nx.DiGraph()
|
||
|
g3 = nx.DiGraph()
|
||
|
g1.add_edge('A', 'B')
|
||
|
g1.add_edge('B', 'C')
|
||
|
g2.add_edge('Y', 'Z')
|
||
|
g3.add_edge('Z', 'Y')
|
||
|
gm12 = iso.DiGraphMatcher(g1, g2)
|
||
|
gm13 = iso.DiGraphMatcher(g1, g3)
|
||
|
x = list(gm12.subgraph_isomorphisms_iter())
|
||
|
y = list(gm13.subgraph_isomorphisms_iter())
|
||
|
assert {'A': 'Y', 'B': 'Z'} in x
|
||
|
assert {'B': 'Y', 'C': 'Z'} in x
|
||
|
assert {'A': 'Z', 'B': 'Y'} in y
|
||
|
assert {'B': 'Z', 'C': 'Y'} in y
|
||
|
assert len(x) == len(y)
|
||
|
assert len(x) == 2
|
||
|
|
||
|
|
||
|
def test_monomorphism_iter1():
|
||
|
g1 = nx.DiGraph()
|
||
|
g2 = nx.DiGraph()
|
||
|
g1.add_edge('A', 'B')
|
||
|
g1.add_edge('B', 'C')
|
||
|
g1.add_edge('C', 'A')
|
||
|
g2.add_edge('X', 'Y')
|
||
|
g2.add_edge('Y', 'Z')
|
||
|
gm12 = iso.DiGraphMatcher(g1, g2)
|
||
|
x = list(gm12.subgraph_monomorphisms_iter())
|
||
|
assert {'A': 'X', 'B': 'Y', 'C': 'Z'} in x
|
||
|
assert {'A': 'Y', 'B': 'Z', 'C': 'X'} in x
|
||
|
assert {'A': 'Z', 'B': 'X', 'C': 'Y'} in x
|
||
|
assert len(x) == 3
|
||
|
gm21 = iso.DiGraphMatcher(g2, g1)
|
||
|
#Check if StopIteration exception returns False
|
||
|
assert not gm21.subgraph_is_monomorphic()
|
||
|
|
||
|
|
||
|
def test_isomorphism_iter2():
|
||
|
# Path
|
||
|
for L in range(2, 10):
|
||
|
g1 = nx.path_graph(L)
|
||
|
gm = iso.GraphMatcher(g1, g1)
|
||
|
s = len(list(gm.isomorphisms_iter()))
|
||
|
assert s == 2
|
||
|
# Cycle
|
||
|
for L in range(3, 10):
|
||
|
g1 = nx.cycle_graph(L)
|
||
|
gm = iso.GraphMatcher(g1, g1)
|
||
|
s = len(list(gm.isomorphisms_iter()))
|
||
|
assert s == 2 * L
|
||
|
|
||
|
|
||
|
def test_multiple():
|
||
|
# Verify that we can use the graph matcher multiple times
|
||
|
edges = [('A', 'B'), ('B', 'A'), ('B', 'C')]
|
||
|
for g1, g2 in [(nx.Graph(), nx.Graph()), (nx.DiGraph(), nx.DiGraph())]:
|
||
|
g1.add_edges_from(edges)
|
||
|
g2.add_edges_from(edges)
|
||
|
g3 = nx.subgraph(g2, ['A', 'B'])
|
||
|
if not g1.is_directed():
|
||
|
gmA = iso.GraphMatcher(g1, g2)
|
||
|
gmB = iso.GraphMatcher(g1, g3)
|
||
|
else:
|
||
|
gmA = iso.DiGraphMatcher(g1, g2)
|
||
|
gmB = iso.DiGraphMatcher(g1, g3)
|
||
|
assert gmA.is_isomorphic()
|
||
|
g2.remove_node('C')
|
||
|
if not g1.is_directed():
|
||
|
gmA = iso.GraphMatcher(g1, g2)
|
||
|
else:
|
||
|
gmA = iso.DiGraphMatcher(g1, g2)
|
||
|
assert gmA.subgraph_is_isomorphic()
|
||
|
assert gmB.subgraph_is_isomorphic()
|
||
|
assert gmA.subgraph_is_monomorphic()
|
||
|
assert gmB.subgraph_is_monomorphic()
|
||
|
# for m in [gmB.mapping, gmB.mapping]:
|
||
|
# assert_true(m['A'] == 'A')
|
||
|
# assert_true(m['B'] == 'B')
|
||
|
# assert_true('C' not in m)
|
||
|
|
||
|
def test_noncomparable_nodes():
|
||
|
node1 = object()
|
||
|
node2 = object()
|
||
|
node3 = object()
|
||
|
|
||
|
# Graph
|
||
|
G = nx.path_graph([node1, node2, node3])
|
||
|
gm = iso.GraphMatcher(G, G)
|
||
|
assert gm.is_isomorphic()
|
||
|
#Just testing some cases
|
||
|
assert gm.subgraph_is_monomorphic()
|
||
|
|
||
|
# DiGraph
|
||
|
G = nx.path_graph([node1, node2, node3], create_using=nx.DiGraph)
|
||
|
H = nx.path_graph([node3, node2, node1], create_using=nx.DiGraph)
|
||
|
dgm = iso.DiGraphMatcher(G, H)
|
||
|
assert dgm.is_isomorphic()
|
||
|
#Just testing some cases
|
||
|
assert gm.subgraph_is_monomorphic()
|