Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemath
GitHub Repository: sagemath/sagelib
Path: blob/master/sage/graphs/bipartite_graph.py
4086 views
1
r"""
2
Bipartite graphs
3
4
This module implements bipartite graphs.
5
6
AUTHORS:
7
8
- Robert L. Miller (2008-01-20): initial version
9
10
- Ryan W. Hinton (2010-03-04): overrides for adding and deleting vertices
11
and edges
12
13
TESTS::
14
15
sage: B = graphs.CompleteBipartiteGraph(7, 9)
16
sage: loads(dumps(B)) == B
17
True
18
19
::
20
21
sage: B = BipartiteGraph(graphs.CycleGraph(4))
22
sage: B == B.copy()
23
True
24
sage: type(B.copy())
25
<class 'sage.graphs.bipartite_graph.BipartiteGraph'>
26
"""
27
28
#*****************************************************************************
29
# Copyright (C) 2008 Robert L. Miller <[email protected]>
30
#
31
# Distributed under the terms of the GNU General Public License (GPL)
32
# http://www.gnu.org/licenses/
33
#*****************************************************************************
34
35
from graph import Graph
36
37
class BipartiteGraph(Graph):
38
r"""
39
Bipartite graph.
40
41
INPUT:
42
43
- ``data`` -- can be any of the following:
44
45
#. Empty or ``None`` (creates an empty graph).
46
#. An arbitrary graph.
47
#. A reduced adjacency matrix.
48
#. A file in alist format.
49
#. From a NetworkX bipartite graph.
50
51
A reduced adjacency matrix contains only the non-redundant portion of the
52
full adjacency matrix for the bipartite graph. Specifically, for zero
53
matrices of the appropriate size, for the reduced adjacency matrix ``H``,
54
the full adjacency matrix is ``[[0, H'], [H, 0]]``.
55
56
The alist file format is described at
57
http://www.inference.phy.cam.ac.uk/mackay/codes/alist.html
58
59
- ``partition`` -- (default: ``None``) a tuple defining vertices of the left and right
60
partition of the graph. Partitions will be determined automatically
61
if ``partition``=``None``.
62
63
- ``check`` -- (default: ``True``) if ``True``, an invalid input partition
64
raises an exception. In the other case offending edges simply won't
65
be included.
66
67
.. NOTE::
68
69
All remaining arguments are passed to the ``Graph`` constructor
70
71
EXAMPLES:
72
73
1. No inputs or ``None`` for the input creates an empty graph::
74
75
sage: B = BipartiteGraph()
76
sage: type(B)
77
<class 'sage.graphs.bipartite_graph.BipartiteGraph'>
78
sage: B.order()
79
0
80
sage: B == BipartiteGraph(None)
81
True
82
83
2. From a graph: without any more information, finds a bipartition::
84
85
sage: B = BipartiteGraph(graphs.CycleGraph(4))
86
sage: B = BipartiteGraph(graphs.CycleGraph(5))
87
Traceback (most recent call last):
88
...
89
TypeError: Input graph is not bipartite!
90
sage: G = Graph({0:[5,6], 1:[4,5], 2:[4,6], 3:[4,5,6]})
91
sage: B = BipartiteGraph(G)
92
sage: B == G
93
True
94
sage: B.left
95
set([0, 1, 2, 3])
96
sage: B.right
97
set([4, 5, 6])
98
sage: B = BipartiteGraph({0:[5,6], 1:[4,5], 2:[4,6], 3:[4,5,6]})
99
sage: B == G
100
True
101
sage: B.left
102
set([0, 1, 2, 3])
103
sage: B.right
104
set([4, 5, 6])
105
106
You can specify a partition using ``partition`` argument. Note that if such graph
107
is not bipartite, then Sage will raise an error. However, if one specifies
108
``check=False``, the offending edges are simply deleted (along with
109
those vertices not appearing in either list). We also lump creating
110
one bipartite graph from another into this category::
111
112
sage: P = graphs.PetersenGraph()
113
sage: partition = [range(5), range(5,10)]
114
sage: B = BipartiteGraph(P, partition)
115
Traceback (most recent call last):
116
...
117
TypeError: Input graph is not bipartite with respect to the given partition!
118
119
sage: B = BipartiteGraph(P, partition, check=False)
120
sage: B.left
121
set([0, 1, 2, 3, 4])
122
sage: B.show()
123
124
::
125
126
sage: G = Graph({0:[5,6], 1:[4,5], 2:[4,6], 3:[4,5,6]})
127
sage: B = BipartiteGraph(G)
128
sage: B2 = BipartiteGraph(B)
129
sage: B == B2
130
True
131
sage: B3 = BipartiteGraph(G, [range(4), range(4,7)])
132
sage: B3
133
Bipartite graph on 7 vertices
134
sage: B3 == B2
135
True
136
137
::
138
139
sage: G = Graph({0:[], 1:[], 2:[]})
140
sage: part = (range(2), [2])
141
sage: B = BipartiteGraph(G, part)
142
sage: B2 = BipartiteGraph(B)
143
sage: B == B2
144
True
145
146
4. From a reduced adjacency matrix::
147
148
sage: M = Matrix([(1,1,1,0,0,0,0), (1,0,0,1,1,0,0),
149
... (0,1,0,1,0,1,0), (1,1,0,1,0,0,1)])
150
sage: M
151
[1 1 1 0 0 0 0]
152
[1 0 0 1 1 0 0]
153
[0 1 0 1 0 1 0]
154
[1 1 0 1 0 0 1]
155
sage: H = BipartiteGraph(M); H
156
Bipartite graph on 11 vertices
157
sage: H.edges()
158
[(0, 7, None),
159
(0, 8, None),
160
(0, 10, None),
161
(1, 7, None),
162
(1, 9, None),
163
(1, 10, None),
164
(2, 7, None),
165
(3, 8, None),
166
(3, 9, None),
167
(3, 10, None),
168
(4, 8, None),
169
(5, 9, None),
170
(6, 10, None)]
171
172
::
173
174
sage: M = Matrix([(1, 1, 2, 0, 0), (0, 2, 1, 1, 1), (0, 1, 2, 1, 1)])
175
sage: B = BipartiteGraph(M, multiedges=True, sparse=True)
176
sage: B.edges()
177
[(0, 5, None),
178
(1, 5, None),
179
(1, 6, None),
180
(1, 6, None),
181
(1, 7, None),
182
(2, 5, None),
183
(2, 5, None),
184
(2, 6, None),
185
(2, 7, None),
186
(2, 7, None),
187
(3, 6, None),
188
(3, 7, None),
189
(4, 6, None),
190
(4, 7, None)]
191
192
::
193
194
sage: F.<a> = GF(4)
195
sage: MS = MatrixSpace(F, 2, 3)
196
sage: M = MS.matrix([[0, 1, a+1], [a, 1, 1]])
197
sage: B = BipartiteGraph(M, weighted=True, sparse=True)
198
sage: B.edges()
199
[(0, 4, a), (1, 3, 1), (1, 4, 1), (2, 3, a + 1), (2, 4, 1)]
200
sage: B.weighted()
201
True
202
203
5. From an alist file::
204
205
sage: file_name = SAGE_TMP + 'deleteme.alist.txt'
206
sage: fi = open(file_name, 'w')
207
sage: fi.write("7 4 \n 3 4 \n 3 3 1 3 1 1 1 \n 3 3 3 4 \n\
208
1 2 4 \n 1 3 4 \n 1 0 0 \n 2 3 4 \n\
209
2 0 0 \n 3 0 0 \n 4 0 0 \n\
210
1 2 3 0 \n 1 4 5 0 \n 2 4 6 0 \n 1 2 4 7 \n")
211
sage: fi.close();
212
sage: B = BipartiteGraph(file_name)
213
sage: B == H
214
True
215
216
6. From a NetworkX bipartite graph::
217
218
sage: import networkx
219
sage: G = graphs.OctahedralGraph()
220
sage: N = networkx.make_clique_bipartite(G.networkx_graph())
221
sage: B = BipartiteGraph(N)
222
223
TESTS:
224
225
Make sure we can create a ``BipartiteGraph`` with keywords but no
226
positional arguments (trac #10958).
227
228
::
229
230
sage: B = BipartiteGraph(multiedges=True)
231
sage: B.allows_multiple_edges()
232
True
233
234
Ensure that we can construct a ``BipartiteGraph`` with isolated vertices
235
via the reduced adjacency matrix (trac #10356)::
236
237
sage: a=BipartiteGraph(matrix(2,2,[1,0,1,0]))
238
sage: a
239
Bipartite graph on 4 vertices
240
sage: a.vertices()
241
[0, 1, 2, 3]
242
sage: g = BipartiteGraph(matrix(4,4,[1]*4+[0]*12))
243
sage: g.vertices()
244
[0, 1, 2, 3, 4, 5, 6, 7]
245
sage: sorted(g.left.union(g.right))
246
[0, 1, 2, 3, 4, 5, 6, 7]
247
248
249
"""
250
251
def __init__(self, data=None, partition=None, check=True, *args, **kwds):
252
"""
253
Create a bipartite graph. See documentation ``BipartiteGraph?`` for
254
detailed information.
255
256
EXAMPLE::
257
258
sage: P = graphs.PetersenGraph()
259
sage: partition = [range(5), range(5,10)]
260
sage: B = BipartiteGraph(P, partition, check=False)
261
"""
262
if data is None:
263
if partition != None and check:
264
if partition[0] or partition[1]:
265
raise ValueError("Invalid partition.")
266
Graph.__init__(self, **kwds)
267
self.left = set()
268
self.right = set()
269
return
270
271
# need to turn off partition checking for Graph.__init__() adding
272
# vertices and edges; methods are restored ad the end of big "if"
273
# statement below
274
import types
275
self.add_vertex = types.MethodType(Graph.add_vertex,
276
self,
277
BipartiteGraph)
278
self.add_vertices = types.MethodType(Graph.add_vertices,
279
self,
280
BipartiteGraph)
281
self.add_edge = types.MethodType(Graph.add_edge, self, BipartiteGraph)
282
283
from sage.structure.element import is_Matrix
284
if isinstance(data, BipartiteGraph):
285
Graph.__init__(self, data, *args, **kwds)
286
self.left = set(data.left)
287
self.right = set(data.right)
288
elif isinstance(data, str):
289
Graph.__init__(self, *args, **kwds)
290
# will call self.load_afile after restoring add_vertex() instance
291
# methods; initialize left and right attributes
292
self.left = set()
293
self.right = set()
294
elif is_Matrix(data):
295
# sanity check for mutually exclusive keywords
296
if kwds.get("multiedges", False) and kwds.get("weighted", False):
297
raise TypeError(
298
"Weighted multi-edge bipartite graphs from reduced " +
299
"adjacency matrix not supported.")
300
Graph.__init__(self, *args, **kwds)
301
ncols = data.ncols()
302
nrows = data.nrows()
303
self.left = set(xrange(ncols))
304
self.right = set(xrange(ncols, nrows + ncols))
305
306
# ensure that the vertices exist even if there
307
# are no associated edges (trac #10356)
308
self.add_vertices(self.left)
309
self.add_vertices(self.right)
310
311
if kwds.get("multiedges", False):
312
for ii in range(ncols):
313
for jj in range(nrows):
314
if data[jj][ii] != 0:
315
self.add_edges([(ii, jj + ncols)] * data[jj][ii])
316
elif kwds.get("weighted", False):
317
for ii in range(ncols):
318
for jj in range(nrows):
319
if data[jj][ii] != 0:
320
self.add_edge((ii, jj + ncols, data[jj][ii]))
321
else:
322
for ii in range(ncols):
323
for jj in range(nrows):
324
if data[jj][ii] != 0:
325
self.add_edge((ii, jj + ncols))
326
elif (isinstance(data, Graph) and partition != None):
327
from copy import copy
328
left, right = partition
329
left = copy(left)
330
right = copy(right)
331
verts = set(left) | set(right)
332
if set(data.vertices()) != verts:
333
data = data.subgraph(list(verts))
334
Graph.__init__(self, data, *args, **kwds)
335
if check:
336
while len(left) > 0:
337
a = left.pop(0)
338
if len(set(data.neighbors(a)) & set(left)) != 0:
339
raise TypeError(
340
"Input graph is not bipartite with " +
341
"respect to the given partition!")
342
while len(right) > 0:
343
a = right.pop(0)
344
if len(set(data.neighbors(a)) & set(right)) != 0:
345
raise TypeError(
346
"Input graph is not bipartite with " +
347
"respect to the given partition!")
348
else:
349
while len(left) > 0:
350
a = left.pop(0)
351
a_nbrs = set(data.neighbors(a)) & set(left)
352
if len(a_nbrs) != 0:
353
self.delete_edges([(a, b) for b in a_nbrs])
354
while len(right) > 0:
355
a = right.pop(0)
356
a_nbrs = set(data.neighbors(a)) & set(right)
357
if len(a_nbrs) != 0:
358
self.delete_edges([(a, b) for b in a_nbrs])
359
self.left, self.right = set(partition[0]), set(partition[1])
360
elif isinstance(data, Graph):
361
Graph.__init__(self, data, *args, **kwds)
362
try:
363
self.left, self.right = self.bipartite_sets()
364
except:
365
raise TypeError("Input graph is not bipartite!")
366
else:
367
import networkx
368
Graph.__init__(self, data, *args, **kwds)
369
if isinstance(data, (networkx.MultiGraph, networkx.Graph)):
370
if hasattr(data, "node_type"):
371
# Assume the graph is bipartite
372
self.left = set()
373
self.right = set()
374
for v in data.nodes_iter():
375
if data.node_type[v] == "Bottom":
376
self.left.add(v)
377
elif data.node_type[v] == "Top":
378
self.right.add(v)
379
else:
380
raise TypeError(
381
"NetworkX node_type defies bipartite " +
382
"assumption (is not 'Top' or 'Bottom')")
383
# make sure we found a bipartition
384
if not (hasattr(self, "left") and hasattr(self, "right")):
385
try:
386
self.left, self.right = self.bipartite_sets()
387
except:
388
raise TypeError("Input graph is not bipartite!")
389
390
# restore vertex partition checking
391
self.add_vertex = types.MethodType(BipartiteGraph.add_vertex,
392
self,
393
BipartiteGraph)
394
self.add_vertices = types.MethodType(BipartiteGraph.add_vertices,
395
self,
396
BipartiteGraph)
397
self.add_edge = types.MethodType(BipartiteGraph.add_edge,
398
self,
399
BipartiteGraph)
400
401
# post-processing
402
if isinstance(data, str):
403
self.load_afile(data)
404
405
return
406
407
def __repr__(self):
408
r"""
409
Returns a short string representation of self.
410
411
EXAMPLE::
412
413
sage: B = BipartiteGraph(graphs.CycleGraph(16))
414
sage: B
415
Bipartite cycle graph: graph on 16 vertices
416
"""
417
s = Graph._repr_(self).lower()
418
if "bipartite" in s:
419
return s.capitalize()
420
else:
421
return "".join(["Bipartite ", s])
422
423
def add_vertex(self, name=None, left=False, right=False):
424
"""
425
Creates an isolated vertex. If the vertex already exists, then
426
nothing is done.
427
428
INPUT:
429
430
- ``name`` -- (default: ``None``) name of the new vertex. If no name
431
is specified, then the vertex will be represented by the least
432
non-negative integer not already representing a vertex. Name must
433
be an immutable object and cannot be ``None``.
434
435
- ``left`` -- (default: ``False``) if ``True``, puts the new vertex
436
in the left partition.
437
438
- ``right`` -- (default: ``False``) if ``True``, puts the new vertex
439
in the right partition.
440
441
Obviously, ``left`` and ``right`` are mutually exclusive.
442
443
As it is implemented now, if a graph `G` has a large number
444
of vertices with numeric labels, then ``G.add_vertex()`` could
445
potentially be slow, if name is ``None``.
446
447
OUTPUT:
448
449
- If ``name``=``None``, the new vertex name is returned. ``None`` otherwise.
450
451
EXAMPLES::
452
453
sage: G = BipartiteGraph()
454
sage: G.add_vertex(left=True)
455
0
456
sage: G.add_vertex(right=True)
457
1
458
sage: G.vertices()
459
[0, 1]
460
sage: G.left
461
set([0])
462
sage: G.right
463
set([1])
464
465
TESTS:
466
467
Exactly one of ``left`` and ``right`` must be true::
468
469
sage: G = BipartiteGraph()
470
sage: G.add_vertex()
471
Traceback (most recent call last):
472
...
473
RuntimeError: Partition must be specified (e.g. left=True).
474
sage: G.add_vertex(left=True, right=True)
475
Traceback (most recent call last):
476
...
477
RuntimeError: Only one partition may be specified.
478
479
Adding the same vertex must specify the same partition::
480
481
sage: bg = BipartiteGraph()
482
sage: bg.add_vertex(0, right=True)
483
sage: bg.add_vertex(0, right=True)
484
sage: bg.vertices()
485
[0]
486
sage: bg.add_vertex(0, left=True)
487
Traceback (most recent call last):
488
...
489
RuntimeError: Cannot add duplicate vertex to other partition.
490
"""
491
# sanity check on partition specifiers
492
if left and right:
493
raise RuntimeError("Only one partition may be specified.")
494
if not (left or right):
495
raise RuntimeError("Partition must be specified (e.g. left=True).")
496
497
# do nothing if we already have this vertex (idempotent)
498
if (name is not None) and (name in self):
499
if (((name in self.left) and left) or
500
((name in self.right) and right)):
501
return
502
else:
503
raise RuntimeError(
504
"Cannot add duplicate vertex to other partition.")
505
506
# add the vertex
507
retval = Graph.add_vertex(self, name)
508
if retval != None: name = retval
509
510
# add to proper partition
511
if left:
512
self.left.add(name)
513
else:
514
self.right.add(name)
515
516
return retval
517
518
def add_vertices(self, vertices, left=False, right=False):
519
"""
520
Add vertices to the bipartite graph from an iterable container of
521
vertices. Vertices that already exist in the graph will not be added
522
again.
523
524
INPUTS:
525
526
- ``vertices`` -- sequence of vertices to add.
527
528
- ``left`` -- (default: ``False``) either ``True`` or sequence of
529
same length as ``vertices`` with ``True``/``False`` elements.
530
531
- ``right`` -- (default: ``False``) either ``True`` or sequence of
532
the same length as ``vertices`` with ``True``/``False`` elements.
533
534
Only one of ``left`` and ``right`` keywords should be provided. See
535
the examples below.
536
537
EXAMPLES::
538
539
sage: bg = BipartiteGraph()
540
sage: bg.add_vertices([0,1,2], left=True)
541
sage: bg.add_vertices([3,4,5], left=[True, False, True])
542
sage: bg.add_vertices([6,7,8], right=[True, False, True])
543
sage: bg.add_vertices([9,10,11], right=True)
544
sage: bg.left
545
set([0, 1, 2, 3, 5, 7])
546
sage: bg.right
547
set([4, 6, 8, 9, 10, 11])
548
549
TEST::
550
551
sage: bg = BipartiteGraph()
552
sage: bg.add_vertices([0,1,2], left=True)
553
sage: bg.add_vertices([0,1,2], left=[True,True,True])
554
sage: bg.add_vertices([0,1,2], right=[False,False,False])
555
sage: bg.add_vertices([0,1,2], right=[False,False,False])
556
sage: bg.add_vertices([0,1,2])
557
Traceback (most recent call last):
558
...
559
RuntimeError: Partition must be specified (e.g. left=True).
560
sage: bg.add_vertices([0,1,2], left=True, right=True)
561
Traceback (most recent call last):
562
...
563
RuntimeError: Only one partition may be specified.
564
sage: bg.add_vertices([0,1,2], right=True)
565
Traceback (most recent call last):
566
...
567
RuntimeError: Cannot add duplicate vertex to other partition.
568
sage: (bg.left, bg.right)
569
(set([0, 1, 2]), set([]))
570
"""
571
# sanity check on partition specifiers
572
if left and right: # also triggered if both lists are specified
573
raise RuntimeError("Only one partition may be specified.")
574
if not (left or right):
575
raise RuntimeError("Partition must be specified (e.g. left=True).")
576
577
# handle partitions
578
if left and (not hasattr(left, "__iter__")):
579
new_left = set(vertices)
580
new_right = set()
581
elif right and (not hasattr(right, "__iter__")):
582
new_left = set()
583
new_right = set(vertices)
584
else:
585
# simplify to always work with left
586
if right:
587
left = map(lambda tf: not tf, right)
588
new_left = set()
589
new_right = set()
590
for tf, vv in zip(left, vertices):
591
if tf:
592
new_left.add(vv)
593
else:
594
new_right.add(vv)
595
596
# check that we're not trying to add vertices to the wrong sets
597
# or that a vertex is to be placed in both
598
if ((new_left & self.right) or
599
(new_right & self.left) or
600
(new_right & new_left)):
601
raise RuntimeError(
602
"Cannot add duplicate vertex to other partition.")
603
604
# add vertices
605
Graph.add_vertices(self, vertices)
606
self.left.update(new_left)
607
self.right.update(new_right)
608
609
return
610
611
def delete_vertex(self, vertex, in_order=False):
612
"""
613
Deletes vertex, removing all incident edges. Deleting a non-existent
614
vertex will raise an exception.
615
616
INPUT:
617
618
- ``vertex`` -- a vertex to delete.
619
620
- ``in_order`` -- (default ``False``) if ``True``, this deletes the
621
`i`-th vertex in the sorted list of vertices,
622
i.e. ``G.vertices()[i]``.
623
624
EXAMPLES::
625
626
sage: B = BipartiteGraph(graphs.CycleGraph(4))
627
sage: B
628
Bipartite cycle graph: graph on 4 vertices
629
sage: B.delete_vertex(0)
630
sage: B
631
Bipartite cycle graph: graph on 3 vertices
632
sage: B.left
633
set([2])
634
sage: B.edges()
635
[(1, 2, None), (2, 3, None)]
636
sage: B.delete_vertex(3)
637
sage: B.right
638
set([1])
639
sage: B.edges()
640
[(1, 2, None)]
641
sage: B.delete_vertex(0)
642
Traceback (most recent call last):
643
...
644
RuntimeError: Vertex (0) not in the graph.
645
646
::
647
648
sage: g = Graph({'a':['b'], 'c':['b']})
649
sage: bg = BipartiteGraph(g) # finds bipartition
650
sage: bg.vertices()
651
['a', 'b', 'c']
652
sage: bg.delete_vertex('a')
653
sage: bg.edges()
654
[('b', 'c', None)]
655
sage: bg.vertices()
656
['b', 'c']
657
sage: bg2 = BipartiteGraph(g)
658
sage: bg2.delete_vertex(0, in_order=True)
659
sage: bg2 == bg
660
True
661
"""
662
# cache vertex lookup if requested
663
if in_order:
664
vertex = self.vertices()[vertex]
665
666
# delete from the graph
667
Graph.delete_vertex(self, vertex)
668
669
# now remove from partition (exception already thrown for non-existant
670
# vertex)
671
try:
672
self.left.remove(vertex)
673
except:
674
try:
675
self.right.remove(vertex)
676
except:
677
raise RuntimeError(
678
"Vertex (%s) not found in partitions" % vertex)
679
680
def delete_vertices(self, vertices):
681
"""
682
Remove vertices from the bipartite graph taken from an iterable
683
sequence of vertices. Deleting a non-existent vertex will raise an
684
exception.
685
686
INPUT:
687
688
- ``vertices`` -- a sequence of vertices to remove.
689
690
EXAMPLES::
691
692
sage: B = BipartiteGraph(graphs.CycleGraph(4))
693
sage: B
694
Bipartite cycle graph: graph on 4 vertices
695
sage: B.delete_vertices([0,3])
696
sage: B
697
Bipartite cycle graph: graph on 2 vertices
698
sage: B.left
699
set([2])
700
sage: B.right
701
set([1])
702
sage: B.edges()
703
[(1, 2, None)]
704
sage: B.delete_vertices([0])
705
Traceback (most recent call last):
706
...
707
RuntimeError: Vertex (0) not in the graph.
708
"""
709
# remove vertices from the graph
710
Graph.delete_vertices(self, vertices)
711
712
# now remove vertices from partition lists (exception already thrown
713
# for non-existant vertices)
714
for vertex in vertices:
715
try:
716
self.left.remove(vertex)
717
except:
718
try:
719
self.right.remove(vertex)
720
except:
721
raise RuntimeError(
722
"Vertex (%s) not found in partitions" % vertex)
723
724
def add_edge(self, u, v=None, label=None):
725
"""
726
Adds an edge from ``u`` and ``v``.
727
728
INPUT:
729
730
- ``u`` -- the tail of an edge.
731
732
- ``v`` -- (default: ``None``) the head of an edge. If ``v=None``, then
733
attempt to understand ``u`` as a edge tuple.
734
735
- ``label`` -- (default: ``None``) the label of the edge ``(u, v)``.
736
737
The following forms are all accepted:
738
739
- ``G.add_edge(1, 2)``
740
- ``G.add_edge((1, 2))``
741
- ``G.add_edges([(1, 2)])``
742
- ``G.add_edge(1, 2, 'label')``
743
- ``G.add_edge((1, 2, 'label'))``
744
- ``G.add_edges([(1, 2, 'label')])``
745
746
See ``Graph.add_edge`` for more detail.
747
748
This method simply checks that the edge endpoints are in different
749
partitions. If a new vertex is to be created, it will be added
750
to the proper partition. If both vertices are created, the first
751
one will be added to the left partition, the second to the right
752
partition.
753
754
TEST::
755
756
sage: bg = BipartiteGraph()
757
sage: bg.add_vertices([0,1,2], left=[True,False,True])
758
sage: bg.add_edges([(0,1), (2,1)])
759
sage: bg.add_edge(0,2)
760
Traceback (most recent call last):
761
...
762
RuntimeError: Edge vertices must lie in different partitions.
763
sage: bg.add_edge(0,3); list(bg.right)
764
[1, 3]
765
sage: bg.add_edge(5,6); 5 in bg.left; 6 in bg.right
766
True
767
True
768
"""
769
# logic for getting endpoints copied from generic_graph.py
770
if label is None:
771
if v is None:
772
try:
773
u, v, label = u
774
except:
775
u, v = u
776
label = None
777
else:
778
if v is None:
779
u, v = u
780
781
# check for endpoints in different partitions
782
if self.left.issuperset((u, v)) or self.right.issuperset((u, v)):
783
raise RuntimeError(
784
"Edge vertices must lie in different partitions.")
785
786
# automatically decide partitions for the newly created vertices
787
if u not in self:
788
self.add_vertex(u, left=(v in self.right or v not in self), right=(v in self.left))
789
if v not in self:
790
self.add_vertex(v, left=(u in self.right), right=(u in self.left))
791
792
# add the edge
793
Graph.add_edge(self, u, v, label)
794
return
795
796
def to_undirected(self):
797
"""
798
Return an undirected Graph (without bipartite constraint) of the given
799
object.
800
801
EXAMPLES::
802
803
sage: BipartiteGraph(graphs.CycleGraph(6)).to_undirected()
804
Cycle graph: Graph on 6 vertices
805
"""
806
return Graph(self)
807
808
def bipartition(self):
809
r"""
810
Returns the underlying bipartition of the bipartite graph.
811
812
EXAMPLE::
813
814
sage: B = BipartiteGraph(graphs.CycleGraph(4))
815
sage: B.bipartition()
816
(set([0, 2]), set([1, 3]))
817
"""
818
return (self.left, self.right)
819
820
def project_left(self):
821
r"""
822
Projects ``self`` onto left vertices. Edges are 2-paths in the
823
original.
824
825
EXAMPLE::
826
827
sage: B = BipartiteGraph(graphs.CycleGraph(20))
828
sage: G = B.project_left()
829
sage: G.order(), G.size()
830
(10, 10)
831
"""
832
G = Graph()
833
G.add_vertices(self.left)
834
for v in G:
835
for u in self.neighbor_iterator(v):
836
G.add_edges([(v, w) for w in self.neighbor_iterator(u)])
837
return G
838
839
def project_right(self):
840
r"""
841
Projects ``self`` onto right vertices. Edges are 2-paths in the
842
original.
843
844
EXAMPLE::
845
846
sage: B = BipartiteGraph(graphs.CycleGraph(20))
847
sage: G = B.project_right()
848
sage: G.order(), G.size()
849
(10, 10)
850
"""
851
G = Graph()
852
G.add_vertices(self.left)
853
for v in G:
854
for u in self.neighbor_iterator(v):
855
G.add_edges([(v, w) for w in self.neighbor_iterator(u)])
856
return G
857
858
def plot(self, *args, **kwds):
859
r"""
860
Overrides Graph's plot function, to illustrate the bipartite nature.
861
862
EXAMPLE::
863
864
sage: B = BipartiteGraph(graphs.CycleGraph(20))
865
sage: B.plot()
866
"""
867
if "pos" not in kwds:
868
kwds["pos"] = None
869
if kwds["pos"] is None:
870
pos = {}
871
left = list(self.left)
872
right = list(self.right)
873
left.sort()
874
right.sort()
875
l_len = len(self.left)
876
r_len = len(self.right)
877
if l_len == 1:
878
pos[left[0]] = [-1, 0]
879
elif l_len > 1:
880
i = 0
881
d = 2.0 / (l_len - 1)
882
for v in left:
883
pos[v] = [-1, 1 - i*d]
884
i += 1
885
if r_len == 1:
886
pos[right[0]] = [1, 0]
887
elif r_len > 1:
888
i = 0
889
d = 2.0 / (r_len - 1)
890
for v in right:
891
pos[v] = [1, 1 - i*d]
892
i += 1
893
kwds["pos"] = pos
894
return Graph.plot(self, *args, **kwds)
895
896
def load_afile(self, fname):
897
r"""
898
Loads into the current object the bipartite graph specified in the
899
given file name. This file should follow David MacKay's alist format,
900
see
901
http://www.inference.phy.cam.ac.uk/mackay/codes/data.html
902
for examples and definition of the format.
903
904
EXAMPLE::
905
906
sage: file_name = SAGE_TMP + 'deleteme.alist.txt'
907
sage: fi = open(file_name, 'w')
908
sage: fi.write("7 4 \n 3 4 \n 3 3 1 3 1 1 1 \n 3 3 3 4 \n\
909
1 2 4 \n 1 3 4 \n 1 0 0 \n 2 3 4 \n\
910
2 0 0 \n 3 0 0 \n 4 0 0 \n\
911
1 2 3 0 \n 1 4 5 0 \n 2 4 6 0 \n 1 2 4 7 \n")
912
sage: fi.close();
913
sage: B = BipartiteGraph()
914
sage: B.load_afile(file_name)
915
Bipartite graph on 11 vertices
916
sage: B.edges()
917
[(0, 7, None),
918
(0, 8, None),
919
(0, 10, None),
920
(1, 7, None),
921
(1, 9, None),
922
(1, 10, None),
923
(2, 7, None),
924
(3, 8, None),
925
(3, 9, None),
926
(3, 10, None),
927
(4, 8, None),
928
(5, 9, None),
929
(6, 10, None)]
930
sage: B2 = BipartiteGraph(file_name)
931
sage: B2 == B
932
True
933
"""
934
# open the file
935
try:
936
fi = open(fname, "r")
937
except IOError:
938
print("Unable to open file <<" + fname + ">>.")
939
return None
940
941
# read header information
942
num_cols, num_rows = map(int, fi.readline().split())
943
max_col_degree, max_row_degree = map(int, fi.readline().split())
944
col_degrees = map(int, fi.readline().split())
945
row_degrees = map(int, fi.readline().split())
946
947
# sanity checks on header info
948
if len(col_degrees) != num_cols:
949
print("Invalid Alist format: ")
950
print("Number of column degree entries does not match number " +
951
"of columns.")
952
return None
953
if len(row_degrees) != num_rows:
954
print("Invalid Alist format: ")
955
print("Number of row degree entries does not match number " +
956
"of rows.")
957
return None
958
959
# clear out self
960
self.clear()
961
self.add_vertices(range(num_cols), left=True)
962
self.add_vertices(range(num_cols, num_cols + num_rows), right=True)
963
964
# read adjacency information
965
for cidx in range(num_cols):
966
for ridx in map(int, fi.readline().split()):
967
# A-list uses 1-based indices with 0's as place-holders
968
if ridx > 0:
969
self.add_edge(cidx, num_cols + ridx - 1)
970
971
#NOTE:: we could read in the row adjacency information as well to
972
# double-check....
973
#NOTE:: we could check the actual node degrees against the reported
974
# node degrees....
975
976
# now we have all the edges in our graph, just fill in the
977
# bipartite partitioning
978
self.left = set(xrange(num_cols))
979
self.right = set(xrange(num_cols, num_cols + num_rows))
980
981
# return self for chaining calls if desired
982
return self
983
984
def save_afile(self, fname):
985
r"""
986
Save the graph to file in alist format.
987
988
Saves this graph to file in David MacKay's alist format, see
989
http://www.inference.phy.cam.ac.uk/mackay/codes/data.html
990
for examples and definition of the format.
991
992
EXAMPLE::
993
994
sage: M = Matrix([(1,1,1,0,0,0,0), (1,0,0,1,1,0,0),
995
... (0,1,0,1,0,1,0), (1,1,0,1,0,0,1)])
996
sage: M
997
[1 1 1 0 0 0 0]
998
[1 0 0 1 1 0 0]
999
[0 1 0 1 0 1 0]
1000
[1 1 0 1 0 0 1]
1001
sage: b = BipartiteGraph(M)
1002
sage: file_name = SAGE_TMP + 'deleteme.alist.txt'
1003
sage: b.save_afile(file_name)
1004
sage: b2 = BipartiteGraph(file_name)
1005
sage: b == b2
1006
True
1007
1008
TESTS::
1009
1010
sage: file_name = SAGE_TMP + 'deleteme.alist.txt'
1011
sage: for order in range(3, 13, 3):
1012
... num_chks = int(order / 3)
1013
... num_vars = order - num_chks
1014
... partition = (range(num_vars), range(num_vars, num_vars+num_chks))
1015
... for idx in range(100):
1016
... g = graphs.RandomGNP(order, 0.5)
1017
... try:
1018
... b = BipartiteGraph(g, partition, check=False)
1019
... b.save_afile(file_name)
1020
... b2 = BipartiteGraph(file_name)
1021
... if b != b2:
1022
... print "Load/save failed for code with edges:"
1023
... print b.edges()
1024
... break
1025
... except:
1026
... print "Exception encountered for graph of order "+ str(order)
1027
... print "with edges: "
1028
... g.edges()
1029
... raise
1030
"""
1031
# open the file
1032
try:
1033
fi = open(fname, "w")
1034
except IOError:
1035
print("Unable to open file <<" + fname + ">>.")
1036
return
1037
1038
# prep: handy lists, functions for extracting adjacent nodes
1039
vnodes = list(self.left)
1040
cnodes = list(self.right)
1041
vnodes.sort()
1042
cnodes.sort()
1043
max_vdeg = max(self.degree(vnodes))
1044
max_cdeg = max(self.degree(cnodes))
1045
num_vnodes = len(vnodes)
1046
vnbr_str = lambda idx: str(idx - num_vnodes + 1)
1047
cnbr_str = lambda idx: str(idx + 1)
1048
1049
# write header information
1050
fi.write("%d %d\n" % (len(vnodes), len(cnodes)))
1051
fi.write("%d %d\n" % (max_vdeg, max_cdeg))
1052
fi.write(" ".join(map(str, self.degree(vnodes))) + "\n")
1053
fi.write(" ".join(map(str, self.degree(cnodes))) + "\n")
1054
for vidx in vnodes:
1055
nbrs = self.neighbors(vidx)
1056
fi.write(" ".join(map(vnbr_str, nbrs)))
1057
fi.write(" 0"*(max_vdeg - len(nbrs)) + "\n")
1058
for cidx in cnodes:
1059
nbrs = self.neighbors(cidx)
1060
fi.write(" ".join(map(cnbr_str, nbrs)))
1061
fi.write(" 0"*(max_cdeg - len(nbrs)) + "\n")
1062
1063
# done
1064
fi.close()
1065
1066
# return self for chaining calls if desired
1067
return
1068
1069
def __edge2idx(self, v1, v2, left, right):
1070
r"""
1071
Translate an edge to its reduced adjacency matrix position.
1072
1073
Returns (row index, column index) for the given pair of vertices.
1074
1075
EXAMPLE::
1076
1077
sage: P = graphs.PetersenGraph()
1078
sage: partition = [range(5), range(5,10)]
1079
sage: B = BipartiteGraph(P, partition, check=False)
1080
sage: B._BipartiteGraph__edge2idx(2,7,range(5),range(5,10))
1081
(2, 2)
1082
"""
1083
try:
1084
if v1 in self.left: # note uses attribute for faster lookup
1085
return (right.index(v2), left.index(v1))
1086
else:
1087
return (right.index(v1), left.index(v2))
1088
except ValueError:
1089
raise ValueError(
1090
"Tried to map invalid edge (%d,%d) to vertex indices" %
1091
(v1, v2))
1092
1093
def reduced_adjacency_matrix(self, sparse=True):
1094
r"""
1095
Return the reduced adjacency matrix for the given graph.
1096
1097
A reduced adjacency matrix contains only the non-redundant portion of
1098
the full adjacency matrix for the bipartite graph. Specifically, for
1099
zero matrices of the appropriate size, for the reduced adjacency
1100
matrix ``H``, the full adjacency matrix is ``[[0, H'], [H, 0]]``.
1101
1102
This method supports the named argument 'sparse' which defaults to
1103
``True``. When enabled, the returned matrix will be sparse.
1104
1105
EXAMPLES:
1106
1107
Bipartite graphs that are not weighted will return a matrix over ZZ::
1108
1109
sage: M = Matrix([(1,1,1,0,0,0,0), (1,0,0,1,1,0,0),
1110
... (0,1,0,1,0,1,0), (1,1,0,1,0,0,1)])
1111
sage: B = BipartiteGraph(M)
1112
sage: N = B.reduced_adjacency_matrix()
1113
sage: N
1114
[1 1 1 0 0 0 0]
1115
[1 0 0 1 1 0 0]
1116
[0 1 0 1 0 1 0]
1117
[1 1 0 1 0 0 1]
1118
sage: N == M
1119
True
1120
sage: N[0,0].parent()
1121
Integer Ring
1122
1123
Multi-edge graphs also return a matrix over ZZ::
1124
1125
sage: M = Matrix([(1,1,2,0,0), (0,2,1,1,1), (0,1,2,1,1)])
1126
sage: B = BipartiteGraph(M, multiedges=True, sparse=True)
1127
sage: N = B.reduced_adjacency_matrix()
1128
sage: N == M
1129
True
1130
sage: N[0,0].parent()
1131
Integer Ring
1132
1133
Weighted graphs will return a matrix over the ring given by their
1134
(first) weights::
1135
1136
sage: F.<a> = GF(4)
1137
sage: MS = MatrixSpace(F, 2, 3)
1138
sage: M = MS.matrix([[0, 1, a+1], [a, 1, 1]])
1139
sage: B = BipartiteGraph(M, weighted=True, sparse=True)
1140
sage: N = B.reduced_adjacency_matrix(sparse=False)
1141
sage: N == M
1142
True
1143
sage: N[0,0].parent()
1144
Finite Field in a of size 2^2
1145
1146
TESTS::
1147
1148
sage: B = BipartiteGraph()
1149
sage: B.reduced_adjacency_matrix()
1150
[]
1151
sage: M = Matrix([[0,0], [0,0]])
1152
sage: BipartiteGraph(M).reduced_adjacency_matrix() == M
1153
True
1154
sage: M = Matrix([[10,2/3], [0,0]])
1155
sage: B = BipartiteGraph(M, weighted=True, sparse=True)
1156
sage: M == B.reduced_adjacency_matrix()
1157
True
1158
1159
"""
1160
if self.multiple_edges() and self.weighted():
1161
raise NotImplementedError(
1162
"Don't know how to represent weights for a multigraph.")
1163
if self.is_directed():
1164
raise NotImplementedError(
1165
"Reduced adjacency matrix does not exist for directed graphs.")
1166
1167
# create sorted lists of left and right edges
1168
left = list(self.left)
1169
right = list(self.right)
1170
left.sort()
1171
right.sort()
1172
1173
# create dictionary of edges, values are weights for weighted graph,
1174
# otherwise the number of edges (always 1 for simple graphs)
1175
D = {}
1176
if self.weighted():
1177
for (v1, v2, weight) in self.edge_iterator():
1178
D[self.__edge2idx(v1, v2, left, right)] = weight
1179
else:
1180
# if we're normal or multi-edge, just create the matrix over ZZ
1181
for (v1, v2, name) in self.edge_iterator():
1182
idx = self.__edge2idx(v1, v2, left, right)
1183
if idx in D:
1184
D[idx] = 1 + D[idx]
1185
else:
1186
D[idx] = 1
1187
1188
# now construct and return the matrix from the dictionary we created
1189
from sage.matrix.constructor import matrix
1190
return matrix(len(self.right), len(self.left), D, sparse=sparse)
1191
1192