-
Notifications
You must be signed in to change notification settings - Fork 94
/
nbody_graph_search.py
executable file
·976 lines (791 loc) · 36.1 KB
/
nbody_graph_search.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
# Author: Andrew Jewett (jewett.aij at g mail)
# http://www.chem.ucsb.edu/~sheagroup
# License: 3-clause BSD License (See LICENSE.TXT)
# Copyright (c) 2012, Regents of the University of California
# All rights reserved.
#__all__ = ['Ugraph', 'GraphMatcher', 'DFS', 'GenError', 'GraphError', 'Disconnected', 'NotUndirected']
import sys
import copy
from operator import itemgetter
class GenError(Exception):
"""
An exception class containing string for error reporting.
"""
def __init__(self, err_msg):
self.err_msg = err_msg
def __str__(self):
return self.err_msg
def __repr__(self):
return str(self)
class GraphError(GenError):
"""
An exception class containing a graph and a string for error reporting.
"""
def __init__(self, g, err_msg):
GenError.__init__(self, err_msg)
self.g = g
def __str__(self):
g_str = str(g)
# If the string representation of the graph is too
# large to fit in one screen, truncate it
g_str_lines = g_str.split('\n')
if (len(g_str_lines) > 12):
g_str_lines = g_str_lines[0:12] + [' ...(additional lines not shown)]']
g_str = '\n'.join(g_str_lines)
return 'Problem with graph:\n'+g_str+'\n'+self.err_msg
def __repr__(self):
return str(self)
class Disconnected(GraphError):
def __init__(self, g, err_msg):
GraphError.__init__(self, g, err_msg)
class NotUndirected(GraphError):
def __init__(self, g, err_msg):
GraphError.__init__(self, g, err_msg)
class Edge(object):
__slots__=["start","stop","attr"]
def __init__(self,
iv_start, # edge starts here (index into vertex list)
iv_stop, # edge ends here (index into vertex list)
attr=None): # edges have an optional type attribute
self.start = iv_start
self.stop = iv_stop
self.attr = attr
def __str__(self):
return '('+str(self.start)+','+str(self.stop)+')'
def __repr__(self):
return str(self)
class Vertex(object):
__slots__=["attr"]
def __init__(self, attr=None):
self.attr = attr
class Dgraph(object):
"""
This class is a minimal implementation of a directed graph.
Vertices and edges are accessed by integer index only (beginning at 0).
Multiple edges connecting the same pair of vertices are allowed.
(One would use the AddEdge() member function to accomplish this.)
Both vertices and edges have an optional "attr" attribute.
"""
NULL = -1 # forbidden vertex id number (used several places)
def __init__(self, edgelist=None):
"""
The constructor accepts an optional neighborlist argument.
This is a simple list of neighbors for every vertex in the graph
and it completely defines the topology of the graph.
(Vertex and edge attributes can be specified later.)
Alternatley, you can leave the neighborlist argument blank,
and build the graph one vertex at a time later
using the "AddVertex()" and "AddEdge()" commands.
(AddEdge() commands must be issued strictly after
all vertices have been defined.)
"""
if edgelist == None:
self.verts = []
self.edges = []
self.nv = 0 #integer keeps track of # of vertices = len(self.verts)
self.ne = 0 #integer keeps track of # of edges = len(self.edges)
self.neighbors = [] # The adjacency list.
else:
# Parse the edge-list format:
iv_max = 0 # <-- what's the vertex with the maximum id number?
for i in range(0, len(edgelist)):
iv = edgelist[i][0]
jv = edgelist[i][1]
if ((iv < 0) or (jv < 0)):
raise(GenError('Error in Dgraph.__init__: Negative vertex number pair encountered: ('+str(iv)+','+str(jv)+')'))
if iv > iv_max:
iv_max = iv
if jv > iv_max:
iv_max = jv
self.nv = iv_max+1
self.verts = [Vertex() for iv in range(0, self.nv)]
self.edges = []
self.ne = 0
self.neighbors = [[] for iv in range(0, self.nv)]
for i in range(0, len(edgelist)):
iv = edgelist[i][0]
jv = edgelist[i][1]
self.neighbors[iv].append(self.ne)
self.edges.append(Edge(iv, jv))
self.ne += 1
assert(self.ne == len(self.edges))
self.SortNeighborLists()
def AddVertex(self, iv=-1, attr=None):
"""
Add a vertex to the graph.
(Edges connected to this vertex must be added later using "AddEdge()"
All vertices should be added before "AddEdge()" is ever invoked.)
Optional "attr" argument allows you to set the attribute of this vertex.
(for example, in a molecule this might correspond to the type of atom
in the molecule).
Optional "iv" argument allows you to specify the index of that vertex.
Vertices can be added in any order, but thei vertex id numbers
should eventually fill the range from 0 to self.nv-1.
"""
if iv == -1: # if iv unspecified, put the vertex at the end of the list
iv = self.nv
if iv < self.nv:
self.verts[iv].attr = attr
else:
# In case there is a gap between iv and nv, fill it with blanks
self.verts += ([Vertex()] * ((1 + iv) - self.nv))
self.neighbors += ([[]] * ((1 + iv) - self.nv))
self.verts[iv].attr = attr
self.nv = iv+1
assert(self.nv == len(self.verts))
assert(self.nv == len(self.neighbors))
def AddEdge(self, iv, jv, attr=None, remove_duplicates=False):
"""
Add an edge to graph connecting vertex iv to jv.
(both are integers from 0 to self.nv-1)
This function must not be called until all vertices have been added.
If the edge is already present (and remove_duplicates==True),
no new edge will be added.
"""
if remove_duplicates:
for je in self.neighbors[iv]:
if jv == self.edges[je].stop:
return # In that case, do nothing, the edge is already present
self.edges.append(Edge(iv, jv, attr))
self.neighbors[iv].append(self.ne)
self.ne += 1
assert(self.ne == len(self.edges))
def ReorderVerts(self, vpermutation, invert=False):
"""
This function allows the user to re-order (relabel) the vertices
in a graph, making the necessary changes to the
self.verts, self.edges, and self.neighbors lists.
By default (invert=False). The vpermutation is a list
from 1 to self.nv which is interpreted this way:
iv = vpermutation[iv_orig]
where "iv" and "iv_orig" are the vertex id numbers before
and after the mapping (which also corresponds to its
position in the self.verts and self.neighbors arrays).
"""
assert(len(self.verts) == self.nv)
assert(len(self.edges) == self.ne)
assert(len(vpermutation) == self.nv)
if (invert):
vperm = [-1 for iv in vpermutation]
for iv in range(0, self.nv):
vperm[ vpermutation[iv] ] = iv
else:
vperm = vpermutation
orig_verts = [vert for vert in self.verts]
for iv_old in range(0, self.nv):
iv = vperm[iv_old]
self.verts[iv] = orig_verts[iv_old]
for ie in range(0, self.ne):
self.edges[ie].start = vperm[self.edges[ie].start]
self.edges[ie].stop = vperm[self.edges[ie].stop]
orig_neighbors = [nlist for nlist in self.neighbors]
# self.neighbors is a 2-d array.
# We need to re-sort "self.neighbors" because the first index is
# a vertex id number, and these id numbers have been permuted.
# However, there's no need to sort the contents of each sub-array
# (self.neighbors[iv]), because these are edge id numbers (indices into
# the self.edges[] array). These edge index numbers are never altered.
# (However the entries stored in self.edges were modified earlier.)
for iv_old in range(0, self.nv):
iv = vperm[iv_old]
self.neighbors[iv] = orig_neighbors[iv_old]
# Optional:
self.SortNeighborLists()
def ReorderEdges(self, epermutation, invert=False):
"""
This function allows the user to re-order (relabel) the
edges in a graph, making the necessary changes to the
self.edges and self.neighbors lists.
By default (invert=False). The epermutation is a list
from 1 to self.ne which is interpreted this way:
ie = epermutation[ie_orig]
where "ie" and "ie_orig" are the edge id numbers before
and after the mapping (which also corresponds to that edge's
position in the self.edges array).
(Minor detail: Recall that in this code, Ugraphs
are implemented by placing two (directed) edges between each pair of
connected, adjacent vertices, which point back-and-forth between them.
Consequently the list of edges in self.edges is often typically
twice as large you might expect.)
"""
assert(len(self.verts) == self.nv)
assert(len(self.edges) == self.ne)
assert(len(epermutation) == self.ne)
if (invert):
eperm = [-1 for ie in epermutation]
for ie in range(0, self.ne):
eperm[ epermutation[ie] ] = ie
else:
eperm = epermutation
orig_edges = [edge for edge in self.edges]
for ie_old in range(0, self.ne):
ie = eperm[ie_old]
self.edges[ie] = orig_edges[ie_old]
for iv in range(0, self.nv):
for j in range(0, len(self.neighbors[iv])):
je_old = self.neighbors[iv][j]
self.neighbors[iv][j] = eperm[je_old]
def SortNeighborLists(self):
assert(self.nv == len(self.neighbors))
for iv in range(0, self.nv):
#Back when self.neighbors was just a 2-dimensional list of
#vertex id numbers, then the following line would have worked:
# self.neighbors[iv].sort()
# ugly python code alert:
#Unfortunately, we had to change the format of self.neighbors. Now
#it is a list of indices into the self.edges array ("ie" numbers).
#We want to sort the "ie" numbers by the vertices they point to.
#self.edge[ie].start should point to the current vertex (hopefully).
#self.edge[ie].stop should point to the vertex it's attached to.
#So we want to sort the ie's in self.neighbors by self.edge[ie].stop
#Create a temporary array of 2-tuples (ie, jv)
nlist = [(ie, self.edges[ie].stop)
for ie in self.neighbors[iv]]
self.neighbors[iv] = [ie for ie,jv in sorted(nlist,
key=itemgetter(1))]
def FindEdge(self, istart, istop):
"""
A simple function looks up the edge id number
corresponding to an edge connecting vertex istart to istop.
If not present returns Dgraph.NULL.
"""
iv = istart
for je in self.neighbors[iv]:
jv = self.edges[je].stop
if jv == istop:
return je
return Dgraph.NULL
def GetVert(self, iv):
return self.verts[iv]
def GetEdge(self, ie):
return self.edges[ie]
def GetNumVerts(self):
return self.nv
def GetNumEdges(self):
return self.ne
# Commenting out. I think it's clearer to use python's deepcopy instead
#def makecopy(self):
# new_copy = Ugraph()
# new_copy.verts = [vertex for vertex in self.verts]
# new_copy.edges = [ edge for edge in self.edges]
# new_copy.neighbors = [nlist for nlist in self.neighbors]
# new_copy.nv = self.nv
# new_copy.ne = self.ne
# return new_copy
def __str__(self):
# Print the graph as a list of neighbor-lists.
# (Note: This is the same format as the first argument to __init__().
# The Vertex.attr and Edge.attr attributes are not printed.)
l = ['([']
for iv in range(0, self.nv):
l.append('[')
for j in range(0, len(self.neighbors[iv])):
je = self.neighbors[iv][j]
jv = self.edges[je].stop
l.append(str(jv))
if j < len(self.neighbors[iv])-1:
l.append(', ')
else:
l.append(']')
if iv < self.nv-1:
l.append(',\n ')
else:
l.append(']')
l.append(',\n [')
for ie in range(0, self.ne):
l.append(str(self.edges[ie]))
if ie < self.ne-1:
l.append(', ')
else:
l.append('])\n')
return ''.join(l)
def __repr__(self):
return str(self)
class Ugraph(Dgraph):
"""
This class is a minimal implementation of an undirected graph.
Vertices and edges are accessed by integer index only (beginning at 0).
Multiple edges connecting the same pair of vertices are allowed.
(One would use the AddEdge() member function to accomplish this.)
Both vertices and edges have an optional "attr" attribute.
Undirected graphs (Ugraphs) are represented internally as
directed graphs. This means that for every edge in the Ugraph,
connecting vertex 2 to 3, for example, two edges are stored
internally, (2 -> 3, and 3 -> 2),
Edges which begin and end at the same vertex are stored only once.)
"""
def __init__(self, edgelist=None):
Dgraph.__init__(self, edgelist)
# Now add the extra edges which point in the reverse direction.
neu = self.ne
ned = self.ne
for ieu in range(0, self.ne):
iv = self.edges[ieu].start
jv = self.edges[ieu].stop
if iv != jv:
ned += 1
self.ieu_to_ied = [Dgraph.NULL for ieu in range(0, neu)]
self.ied_to_ieu = [Dgraph.NULL for ied in range(0, ned)]
ied_redundant = neu
for ie in range(0, neu):
iv = self.edges[ie].start
jv = self.edges[ie].stop
attr = self.edges[ie].attr
self.ieu_to_ied[ie] = ie
self.ied_to_ieu[ie] = ie
if iv != jv:
# Then create another edge which points in the reverse direction
Dgraph.AddEdge(self, jv, iv, attr) # <--this increments self.ne
self.ied_to_ieu[ied_redundant] = ie
ied_redundant += 1
self.neu = neu
assert(self.ne == ned)
def AddEdge(self, iv, jv, attr=None, remove_duplicates=False):
"""
Add an edge to an undirected graph connecting vertices iv and jv.
If the edge is already present (and remove_duplicates==True),
no new edge will be added.
Note: Undirected Ugraphs are implemented by creating two separate
digraph edges that conect iv->jv and jv->iv.
"""
self.ieu_to_ied.append( len(self.edges) )
Dgraph.AddEdge(self, iv, jv, attr, remove_duplicates)
self.ied_to_ieu.append( self.neu )
if jv != iv:
Dgraph.AddEdge(self, jv, iv, attr, remove_duplicates)
self.ied_to_ieu.append( self.neu )
self.neu += 1
assert(len(self.ieu_to_ied) == self.neu)
assert(len(self.ied_to_ieu) == len(self.edges))
def ReorderEdges(self, epermutation, invert=False):
Dgraph.ReorderEdges(self, epermutation, invert)
# Now update the
# self.ieu_to_ied and
# self.ied_to_ieu lookup tables:
if (invert): # (first invert the permutation if necessary)
eperm = [-1 for ie in epermutation]
for ie in range(0, self.ne):
eperm[ epermutation[ie] ] = ie
else:
eperm = epermutation
#epermutation.reverse()
ieu_to_ied_orig = [ied for ied in self.ieu_to_ied]
ied_to_ieu_orig = [ieu for ieu in self.ied_to_ieu]
for ieu in range(0, self.neu):
ied_old = ieu_to_ied_orig[ieu]
ied = eperm[ied_old]
self.ieu_to_ied[ieu] = ied
for ied_old in range(0, self.ne):
ieu = ied_to_ieu_orig[ied_old]
ied = eperm[ied_old]
self.ied_to_ieu[ied] = ieu
eperm = epermutation
def LookupDirectedEdgeIdx(self, ieu):
return self.ieu_to_ied[ieu]
def LookupUndirectedEdgeIdx(self, ied):
return self.ied_to_ieu[ied]
#def GetVert(self, iv): <-- (inherited from parent)
# return self.verts[iv]
def GetEdge(self, ieu):
ied = self.ieu_to_ied[ieu]
return self.edges[ied]
#def GetNumVerts(self): <-- (inherited from parent)
# return self.nv
def GetNumEdges(self):
return self.neu
def FindEdge(self, istart, istop):
"""
A simple function looks up the (undirected) edge id number
corresponding to an edge connecting vertices istart and istop.
If not present returns Dgraph.NULL.
To find the corresponding entry in the self.edges[] list,
you can either:
use the LookupDirectedEdge() lookup function
or
you can use the parent-class' version of this function
Dgraph.FindEdge(self, istart, istop) which returns
this number by default.
"""
ied = Dgraph.FindEdge(self, istart, istop)
ieu = self.LookupUndirectedEdgeIdx(ied)
return ieu
def CalcEdgeLookupTable(self):
"""
COMMENT: THIS NEXT FUNCTION IS PROBABLY NOT NECESSARY AND MIGHT BE
REMOVED AT A LATER TIME WHEN I FIGURE OUT A BETTER WAY.
Because undirected graphs (Ugraphs) are implemented as directed graphs
(Dgraphs) with redundant edges, they may have some extra edges which
the user never explicitly asked for.
There is some confusion about whether the i'th edge refers to
the i'th undirected edge that the user explicitly added, or
the i'th directed edge which is stored internally.
(The number of directed edges is usually twice the number of
edges that the user asked for. But not always, because edges
wich start and end at the same vertex are only represented once.)
This function calculates lookup tables to translate between
the two edge numbering systems:
self.ieu_to_ied[ieu] returns a directed edge id number,
(which is an index into the self.edges list)
corresponding to the ieu'th undirected edge
which was explicitly added by the caller.
self.ied_to_ieu[ied] takes a directed edge id number (ied,
an index into the self.edges list)
and returns the undirected edge number,
which is allways <= ied
"""
self.ieu_to_ied = []
self.ied_to_ieu = [Ugraph.NULL for ied in range(0, self.ne)]
for ied in range(0, self.ne):
iv = self.edges[ied].start
jv = self.edges[ied].stop
ieu = len(self.ieu_to_ied)
self.ied_to_ieu[ied] = ieu
if iv <= jv:
self.ieu_to_ied.append(ied)
def SortVertsByDegree(g):
vert_numneighbors = [(iv, len(g.neighbors[iv])) for iv in range(0, g.nv)]
vert_numneighbors.sort(key=itemgetter(1))
order = [vert_numneighbors[iv][0] for iv in range(0, g.nv)]
g.ReorderVerts(order, invert=True)
class DFS(object):
"""
This class contains a member function (Order()) calculates the order
of vertices visited in a depth-first-search over a connected graph.
"""
def __init__(self, g):
self.g = g
self.sv = 0 #integer sv keeps track of how many vertices visited so far
self.se = 0 #integer se keeps track of how many edges visited so far
self.vvisited=[False for iv in range(0, self.g.nv)] # verts visited
self.vorder =[Dgraph.NULL for iv in range(0, self.g.nv)] #search order
self.evisited=[False for ie in range(0, self.g.ne)] # edges visited
self.eorder =[Dgraph.NULL for ie in range(0, self.g.ne)] #search order
def Reset(self):
self.sv = 0
self.se = 0
for iv in range(0, self.g.nv):
self.vvisited[iv] = False
self.vorder[iv] = Dgraph.NULL
for ie in range(0, self.g.ne):
self.evisited[ie] = False
self.eorder[ie] = Dgraph.NULL
def Order(self, starting_node=0):
"""
VisitOrder(starting_node)
generates a list of integers from 0 to self.g.nv-1 (=#vertices minus 1)
which represents the order in which the vertices would be visited
during a Depth-First-Search.
The first vertex visited is specified by the "starting_node" argument
(an integer (from 0 to g.nv-1)).
"""
self.Reset()
# The first vertex to be visited should be the starting_node
self.vorder[0] = starting_node
self.vvisited[starting_node] = True
self.sv = 1
self._Order(starting_node)
if self.sv != self.g.nv:
raise(Disconnected(self.g, "Error(Order): "+
"The input graph is not connected."))
assert(self.se == self.g.ne)
return ([iv for iv in self.vorder], [ie for ie in self.eorder])
#return self.order
def _Order(self, iv):
"""
_Order() is a recursive function which carries out a
Depth-First-Search over the graph "self.g", starting with vertex iv.
"""
for je in self.g.neighbors[iv]:
jv = self.g.edges[je].stop
if not self.evisited[je]:
self.eorder[self.se] = je
self.se += 1
self.evisited[je] = True
if not self.vvisited[jv]:
self.vorder[self.sv] = jv
self.sv += 1
self.vvisited[jv] = True
self._Order(jv)
def IsConnected(self):
self.Reset()
self._Order(0)
return (self.sv == self.g.nv)
def IsCyclic(self):
"""
IsCyclic() returns True if the graph is cyclic (and connected).
(An exception is raised on disconnected graphs.)
This function quits early as soon as a cycle is found.
"""
self.Reset()
if (type(self.g) is Ugraph):
is_cyclic = self._IsCyclicUgraph(0, Dgraph.NULL)
else:
is_cyclic = self._IsCyclic(0)
if ((self.sv != self.g.nv) and (not is_cyclic)):
raise(Disconnected(self.g, "Error(IsCyclic): "+
"The input graph is not connected."))
return is_cyclic
def _IsCyclicUgraph(self, iv, ivprev):
"""
_IsCyclicUgraph() is a recursive function which carries out a
Depth-First-Search over the graph "self.g" to determine whether the
graph is cyclic. This function works on undirected graphs (Ugraphs).
Indirected graphs (Ugraphs) are a special case.
Ugraphs are implemented by using two (redundant) forward/backward edges
connecting each pair of adjacent vertices. This creates trivial loops.
This version of _IsCyclicUgraph() only counts loops between more
distantly connected vertices.
"""
self.sv += 1
self.vvisited[iv] = True
for je in self.g.neighbors[iv]:
jv = self.g.edges[je].stop
if self.vvisited[jv]:
if jv != ivprev:
return True
elif self._IsCyclicUgraph(jv, iv):
return True
return False
def _IsCyclic(self, iv):
"""
_IsCyclic() is a recursive function which carries out a
Depth-First-Search over the graph "self.g" to determine whether
the graph is cyclic.
This function works on directed graphs.
"""
self.sv += 1
self.vvisited[iv] = True
for je in self.g.neighbors[iv]:
jv = self.g.edges[je].stop
if self.vvisited[jv]:
return True
elif self._IsCyclic(jv):
return True
return False
class GraphMatcher(object):
"""
This class is a variant of the VF2 algorithm for searching
for small connected subgraphs (g) within a larger graph (G).
GraphMatcher works on directed or underected graphs (Dgraph or Ugraph).
This particular version is better optimized for detecting subgraph
isomorphisms between two graphs of highly unequal size. It should be
faster in these situations because, the computation required for
each step is independent of the number of vertices in the larger graph
In the original VF2 algorithm, the computation time for each step
is proportional to the number of vertices in the larger graph.
(The distinction matters when one graph is much smaller than the other.)
Limitations: At the moment, the matching process uses a simple
depth-first-search to search the vertices of the small graph "g".
Hence this approach fails when the smaller graph g is disconnected.
(but it can probably be fixed by picking a different algorithm to search
the small graph).
"""
def __init__(self,
G, # The "big" graph
g): # The little graph (number of vertices in g must be <= G)
self.G = G
self.g = copy.deepcopy(g)
if (type(self.G) is Ugraph):
assert(type(self.g) is Ugraph)
# self.G.CalcEdgeLookupTable() <-- not needed anymore
self.sv = 0
self.se = 0
self.voccupiedG = [False for iv in range(0, G.nv)]
self.eoccupiedG = [False for ie in range(0, G.ne)]
self.G_is_too_small = False
if ((g.nv > G.nv) or
(g.ne > G.ne)):
self.G_is_too_small = True
#raise GenErr('Error: The first argument of GraphMatcher(G,g),\n'+
# ' must be at least as large as the second.')
# The list self.iv_to_Iv is the mapping between the graph vertices.
# Iv is an index into the large graph's list of vertices.
# iv is an index into the small graph's list of vertices.
# The mapping is stored in the iv_to_Iv list.
self.iv_to_Iv = [Dgraph.NULL for Iv in range(0, self.g.nv)]
self.ie_to_Ie = [Dgraph.NULL for Ie in range(0, self.g.ne)]
# (This used to be called "core_2" in the VF2 algorithm)
# Due to the large number of recursion limit
self.old_recursion_limit = sys.getrecursionlimit()
expected_max_recursion = self.g.nv
if self.old_recursion_limit < 1.5 * expected_max_recursion:
# Give some breathing room.
sys.setrecursionlimit(int(1.5 * expected_max_recursion))
subgraph_searcher = DFS(self.g)
# Perform a Depth-First-Search on the small graph.
self.vorder_g, self.eorder_g = subgraph_searcher.Order()
# Then re-order the vertices and edgers to
# match the order they were visited.
# Note on permutation order:
# (The DFS.Order() function returns the permutation in this format
# old_index[ new_index ]
# where new_index is the DFS iteration when the vertex/edge was visited
# and old_index is the original vertex/edge order.
# However the ReorderVerts() and ReorderEdges() functions expect
# the permutation to have the opposite order: new_index[ old_index ]
# Hence we set "invert=True", when we invoke these functions.)
self.g.ReorderVerts(self.vorder_g, invert=True)
self.g.ReorderEdges(self.eorder_g, invert=True)
# Initialize state
self.Reset()
def Reset(self):
"""Reinitializes the state of the match-search algorithm.
"""
for iv in range(0, self.g.nv):
self.iv_to_Iv[iv] = Dgraph.NULL
for ie in range(0, self.g.ne):
self.ie_to_Ie[ie] = Dgraph.NULL
for Iv in range(0, self.G.nv):
self.voccupiedG[Iv] = False
for Ie in range(0, self.G.ne):
self.eoccupiedG[Ie] = False
self.se = 0
self.sv = 0
# OPTIONAL: First, do a partial sort for the vertices in the graphs
# based on number of edges emanating from each vertex.
# (This is probably unnecessary for small subgraphs.)
#SortVertsByDegree(self.g)
def Matches(self):
"""
Iterator over all matches between G and g.
Each "match" corresponds to a subgraph of G which is isomorphic to g.
Matches is formatted as a 2-tuple of lists:
(list of vertex ids from G, list of edge ids from G)
The vertex ids in the list are a subset of the integers from 0 to G.nv.
The edge ids in the list are a subset of the integers from 0 to G.ne.
(The corresponding vertices and edges from g are indicated by the order)
"""
self.Reset()
if self.G_is_too_small:
# Then there are fewer verts and edges in G than in g.
# Thus it is impossible for a subgraph of G to be isomorphic to g.
return # return no matches
for Iv in range(0, self.G.nv):
# match vertex Iv from G with vertex 0 from graph g
self.iv_to_Iv[0] = Iv
self.voccupiedG[Iv] = True
# Implementation:
# In this loop we begin the search process
# starting with a different vertex (Iv) from big graph G,
# and matching it with the first vertex (iv=0) from small graph g.
# In this way the match "begins" from vertex Iv in G.
#
# Any matches found which begin from vertex Iv are distinct
# from matches beginning from any other vertex in G.
# Looping over all Iv in G is necessary and sufficient
# to insure that all possible subgraphs of G
# (which are isomorphic to g) are considered.
self.sv = 1 # we have matched one vertex already
self.se = 0 # we haven't matched any edges yet
for match in self.Match():
yield match
self.voccupiedG[Iv] = False
def Match(self):
# self.se represents how many vertices have been matched so far.
# We are done searching if all of the edges from 0 to self.se-1
# from graph g have been selected (matched with edges from graph G).
if self.se == self.g.ne:
# Note: This also gaurantees that all vertices have been visited.
assert(self.sv == self.g.nv)
yield self.ReformatMatch()
else:
# VF2-style recursive loop:
# We know the next edge to be matched is connected to at least
# one previously visited vertex from g which has already been
# been added to the the current match-in-progress.
iv = self.g.edges[self.se].start
Iv = self.iv_to_Iv[iv]
assert(iv < self.sv) # <-- check to verify this is so
# The other vertex may or may not have been visited (matched) yet.
iv_neighbor = self.g.edges[self.se].stop
# Two cases:
# Case 1: edge self.se points to a previously visited vertex from g
# This means we have a loop.
if iv_neighbor < self.sv:
# In that case, then the corresponding edge in G must
# connect the corresponding pair of vertices from G.
# (Which we know have already been assigned to vertices in g
# because both iv and iv_neighbor are < self.sv)
Iv_neighbor = self.iv_to_Iv[iv_neighbor]
# Loop over all of the edges in G which connect this pair
# of vertices (Iv --> Iv_neighbor)
for Je in self.G.neighbors[Iv]:
Jv = self.G.edges[Je].stop
if ((Jv == Iv_neighbor) and
(not self.eoccupiedG[Je])):
# Match edge Je from big graph G with
# edge self.se from small graph g
self.ie_to_Ie[self.se] = Je
self.se += 1
self.eoccupiedG[Je] = True
for match in self.Match():
yield match
self.eoccupiedG[Je] = False
self.se -= 1
self.ie_to_Ie[self.se] = Dgraph.NULL
# Case 2:
else: # this would mean that iv_neighbor >= self.sv
# If iv_neighbor>=self.sv, then this edge points to to a vertex
# in g which has not yet been paired with a vertex from G.
# Loop over all of the edges in G which connect vertex
# Iv from G to new (unvisited) vertices in G
for Je in self.G.neighbors[Iv]:
Jv = self.G.edges[Je].stop
if (not self.voccupiedG[Jv]):
assert(not self.eoccupiedG[Je])
# Match both edge Je with je
# AND vertex Jv with jv
self.ie_to_Ie[self.se] = Je
self.se += 1
self.eoccupiedG[Je] = True
self.iv_to_Iv[self.sv] = Jv
self.sv += 1
self.voccupiedG[Jv] = True
# Then continue the recursion
for match in self.Match():
yield match
self.voccupiedG[Jv] = False
self.sv -= 1
self.iv_to_Iv[self.sv] = Dgraph.NULL
self.eoccupiedG[Je] = False
self.se -= 1
self.ie_to_Ie[self.se] = Dgraph.NULL
def ReformatMatch(self):
# (This is because we are assuming g is connected.
# IT should not have any orphanned vertices.)
# Now return the match:
#
# There are different ways of doing this
# version 1:
#match = (self.iv_to_Iv, self.ie_to_Ie) <-return a pointer to array
# version 2:
#match = ([Iv for Iv in self.iv_to_Iv], <-return a copy of the array
# [Ie for Ie in self.ie_to_Ie])
# version 3:
# Recall that the vertices and edges and g have been re-ordered,
# so sort the list of Iv indices in the order they would be
# matched with the original vertices from the original graph g:
#match = ([self.iv_to_Iv[self.vorder_g[iv]]
# for iv in range(0,self.g.nv)],
# [self.ie_to_Ie[self.eorder_g[ie]]
# for ie in range(0,self.g.ne)])
# version 4: Similar to version 3 above, but we also translate
# the directed edge id list into a shorter undirected
# edge id list.
match_verts = [self.iv_to_Iv[self.vorder_g[iv]]
for iv in range(0,self.g.nv)]
if type(self.g) is Dgraph:
match_edges = [self.ie_to_Ie[self.eorder_g[ie]]
for ie in range(0, self.g.ne)]
else:
#assert(atype(self.g) is Ugraph)
match_edges = [Dgraph.NULL for ieu in range(0, self.g.neu)]
for ie in range(0, self.g.ne):
iv = self.g.edges[ie].start
jv = self.g.edges[ie].stop
if iv <= jv: # <-- avoid duplicating edges (iv,jv) and (jv,iv)
ieu = self.g.LookupUndirectedEdgeIdx(ie)
Ie = self.ie_to_Ie[ie]
Ieu = self.G.LookupUndirectedEdgeIdx(Ie)
match_edges[ieu] = Ieu
return (tuple(match_verts), tuple(match_edges))