Improved version of the w-algorithm for the graph isomorphism testing


back to my homepage

This is the improved version of the w-algorithm for the graph isomorphism testing (in Python). This algorithm was suggested and studied in:
Czimmermann P., The Suggestion of an Algorithm for the Graph Isomorphism Testing (in Slovak), Zborník 6.vedeckej konferencie doktorandov, Nitra, ISBN 80-8050-813-5, 2005
Czimmermann P., On Certain Algorithm for Graph Isomorphism Problem, Journal of Information, Control and Management Systems, Vol.3, No.1, ISSN 1336-1716, 2005
Czimmermann P., An Algebraic Approach to Graph Isomorphism Testing, Journal of Information, Control and Management Systems, Vol.4, No.1, ISSN 1336-1716, 2006
Czimmermann P., Connections between the Graph Isomorphism and the Number of Walks in Graphs, Zborník konferencie APLIMAT, Bratislava, 2007

Main functions are check_iso (w-algorithm) and check_LC (its improved version).
  
from scipy import *
from sets import Set
from copy import copy
import pysqlite2.dbapi2 as sqlite
from scipy.stats import random_integers
# -------------------------------------

def get_nwalk(A,nk=None,X=None):
    """U=get_nwalk(A,nk=None,X=None)
    
       For A being (n x n) adjacency matrix, returns n-vector U
       where U[i] is the matrix with rows U[i]={s_i_1,s_i_2,...,s_i_nk};
       s_i_k is the number of (generalised) walks from vertex i to vertex k. 
       If nk is not given, then nk = 2*n. Default value for X is E_n
       (classical walks). 
    """

    m,n=A.shape
    if not nk:
        nk=2*n
    U=zeros((n,n,nk))
    if not X:
        Ap=copy(A)
    else:
        Ap=copy(dot(A,X))
        
    for c in range(nk):
        for k in range(n):
            U[k,:,c]=Ap[:,k]
        Ap=dot(Ap,A)
    return U


def get_UV(A,B,nk=None,X=None):
    """U,V=get_UV(A,B,nk=None,X=None)

       For A,B - the (n x n) adjacency matrices, returns n-vectors U, V
       where U[i] is the matrix with rows U[i]={s_i_1,s_i_2,...,s_i_nk};
       s_i_k is the number of (generalised) walks from vertex i to vertex k
       in graph with adjacency matrix A. The same for vector V and matrix B. 
       If nk is not given, then nk = 2*n. Default value for X is E_n
       (classical walks). 
    
    """

    return get_nwalk(A,nk,X), get_nwalk(B,nk,X)


def local_complement(A,nv):
    """AL=local_complement(A,nv)

       Returns adjacency matrix of local complement of vertex nv
       (vertex numbers are 0,1,...) in graph with adjacency matrix A.  
    """
    
    Anv=copy(A)
    nzind=A[nv].nonzero()
    for r in nzind:
        for s in nzind:
            Anv[r,s]=1-A[r,s]
            Anv[s,s]=0
    return Anv


def check_iso(A,B,nk=None,X=None):
    """check_iso(A,B,nk=None,X=None)
    
    Isomorphism check of graphs with adjac. matrices A,B,
    using (generalised, if X is given) walks of length 1,...,nk.
    Returns False, if graphs are distinguishable, 
    otherwise returns the permutation P of vertices such that
    row- and column-permuted B could be A. 
    """
    
    n,m=A.shape
    if not nk:
        nk=2*n
    U,V=get_UV(A,B,nk)
    P=[-1]*n
    arr2d_tuple=lambda D: tuple([tuple(p) for p in D])
    
    UL=[Set(arr2d_tuple(p)) for p in U]
    VL=[Set(arr2d_tuple(p)) for p in V]

    for i in range(n):
        for j in range(n):
            if UL[i]==VL[j] and all(U[i][i]==V[j][j]) and not j in P:
                P[i]=j; break
        if P[i]==-1:
            return False
    return P


def check_LC(A,B,nk=3,chk_iso=0):
    """check_LC(A,B,nk=3,chk_iso=0)
    
    Isomorphism check of graphs with adjac. matrices A,B,
    using (generalised, if X is given) walks of length 1,...,nk.
    If chk_iso is nonzero, original graphs are first checked.
    In case they are indistinguishable, we form all possible
    tuples of local complements of A,B and make checks with them.
    
    Returns False, if graphs are distinguishable, 
    otherwise returns the permutation P of vertices such that
    row- and column-permuted B could be A. 
    """


    if chk_iso:
        if not check_iso(A,B,nk):
            return False
    m,n=A.shape
    P=[-1]*n
    for i in range(n):
        for j in range(n):
            A_loc,B_loc=local_complement(A,i), local_complement(B,j)
            if check_iso(A_loc,B_loc,nk) and not j in P:
                P[i]=j; break
        if P[i]==-1:
            return False 
    return P

def check_XW(A,B,nk=3,chk_iso=0):
    pass
    

def inv_perm(P):
    """IP=inv_perm(P)
    
       Returns inverse permutation IP to permutation P.
    """
    Pz=zip(P,range(len(P)))
    Pz.sort()
    return [p[1] for p in Pz]
    

def check_perm(A,B,P):
    """check_perm(A,B,P)
    
       Check, if the given permutation P applied to rows and columns
       of matrix B is the matrix A (returns True or False).
    """
    Bm=B[P]
    BP=Bm.transpose()[P].transpose()
    return all(BP==A)

# ------------ reading unlabelled graph from graphDB --------------
def read_unlab32(fname):           # works only for 32 bit machines
    S=file(fname,'r').read()
    D=fromstring(S,dtype=Int16)
    n=D[0]
    A=zeros((n,n))
    pos=1
    for v in range(n):
        dv=D[pos]
        if not dv:
            print "Isolated vertex %d" %v
        U=D[pos+1:pos+dv+1];  pos += (dv+1)
        for u in U:
            A[v,u]=1
    return A


def readword(S,pos):
    return ord(S[pos]) | (ord(S[pos+1]) << 8)
  

def read_unlab(fname):
    """A=read_unlab(fname)

       Read an unlabeled graph from file (in GraphDB format) with name fname.
       Returns the adjacency matrix A.
    """
    S=file(fname,'r').read()
    n=readword(S,0)

    pos=2
    A=zeros((n,n),dtype=Int32)

    for k in range(n):
        nv=readword(S,pos); pos += 2
        if not nv:
            print "Isolated vertex %d" %k
        else:
            for j in range(nv):
                v_node=readword(S,pos); pos += 2
                A[k,v_node]=1     
    return A


# ------------- reading regular graphs from database --------------

def get_conn(dbname='srg.db'):
    con=sqlite.connect(dbname)
    cur=con.cursor()
    return con,cur


# ----------------------- helper functions ------------------------
def get_gid(n,deg,n_adj,n_far,dbname='srg.db'):
    con,cur=get_conn(dbname)
    qr="select g_id from srg where n=%d and deg=%d "\
       "and n_adj=%d and n_far=%d" %(n,deg,n_adj,n_far)
    cur.execute(qr)  
    return cur.fetchone()[0]


def str_tomat(s,n):
    M1=array([int(c) for c in s])
    return M1.reshape(n,n)
# -----------------------------------------------------------------


def get_srgcount(n,deg,n_adj,n_far,dbname='srg.db'):
    con,cur=get_conn(dbname)
    g_id=get_gid(n,deg,n_adj,n_far,dbname)    
    qr="select count(*) from srg_mat where g_id=%d" %g_id
    cur.execute(qr)
    return cur.fetchone()[0] or 0


def get_avail_srg(g_count=None,dbname='srg.db'):
    """GA=get_avail_srg(g_count=None,dbname='srg.db')
    
       Returns the list of types (and counts, if g_count is nonzero)
       of strongly regular graphs contained in our database.
       The rows of GA are of the form: ((26, 10, 3, 4), 10).
    """
    con,cur=get_conn(dbname)
    qr_all='select n,deg,n_adj,n_far from srg'
    cur.execute(qr_all)
    res=cur.fetchall()
    res.sort()
    if g_count:
        for k in range(len(res)):
            n,deg,n_adj,n_far=res[k]
            ng=get_srgcount(n,deg,n_adj,n_far)
            res[k]=res[k],ng           
    return res

        
def get_srgmat(n,deg,n_adj,n_far,get_all=True,dbname='srg.db'):
    """LA=get_srgmat(n,deg,n_adj,n_far,get_all=True,dbname='srg.db')
    
       Gets list of adjac. matrices of SRG-s of type (n,deg,n_adj,n_far).
       If get_all is false, returns one of matrices, randomly choosen.     
    """
    con,cur=get_conn(dbname)    
    g_id=get_gid(n,deg,n_adj,n_far,dbname)
    qr="select adjmat from srg_mat where g_id=%d" %g_id
    cur.execute(qr)
    res=cur.fetchall()
    M_list=[]
    if get_all:
        for Ms, in res:
            M_list += [str_tomat(Ms,n)]        
        return M_list
    else:
        return str_tomat(res[random_integers(0,len(res)-1)][0],n)
    

iso_methods={'LC':check_LC,'XW':check_XW}

def check_srg_group(n,deg,n_adj,n_far,method='LC',dbname='srg.db'):
    """check_srg_group(n,deg,n_adj,n_far,method='LC',dbname='srg.db')

       Perform check of distinguashability of SRG-s of the given
       type (n,deg,n_adj,n_far) with 'Local Complement' checks
       if method='LC' or with generalised walks check, if method='XW'.
       Returns False, if all graphs in group are distinguishable,
       or the indices (n1,n2) of indistinguishable graphs
       (in the list returned by 'get_srgmat').
    """
    
    LA=get_srgmat(n,deg,n_adj,n_far,dbname)
    if not LA:
        raise ValueError, "No such group of SRG-s"

    ng=len(LA)
    check_fcn=iso_methods[method]
    res=False
    ind2=[(p,q) for p in range(ng) for q in range(ng) if p