1"""
2Generators and functions for bipartite graphs.
3"""
4
5import math
6import numbers
7from functools import reduce
8
9import networkx as nx
10from networkx.utils import nodes_or_number, py_random_state
11
12__all__ = [
13 "configuration_model",
14 "havel_hakimi_graph",
15 "reverse_havel_hakimi_graph",
16 "alternating_havel_hakimi_graph",
17 "preferential_attachment_graph",
18 "random_graph",
19 "gnmk_random_graph",
20 "complete_bipartite_graph",
21]
22
23
24@nx._dispatchable(graphs=None, returns_graph=True)
25@nodes_or_number([0, 1])
26def complete_bipartite_graph(n1, n2, create_using=None):
27 """Returns the complete bipartite graph `K_{n_1,n_2}`.
28
29 The graph is composed of two partitions with nodes 0 to (n1 - 1)
30 in the first and nodes n1 to (n1 + n2 - 1) in the second.
31 Each node in the first is connected to each node in the second.
32
33 Parameters
34 ----------
35 n1, n2 : integer or iterable container of nodes
36 If integers, nodes are from `range(n1)` and `range(n1, n1 + n2)`.
37 If a container, the elements are the nodes.
38 create_using : NetworkX graph instance, (default: nx.Graph)
39 Return graph of this type.
40
41 Notes
42 -----
43 Nodes are the integers 0 to `n1 + n2 - 1` unless either n1 or n2 are
44 containers of nodes. If only one of n1 or n2 are integers, that
45 integer is replaced by `range` of that integer.
46
47 The nodes are assigned the attribute 'bipartite' with the value 0 or 1
48 to indicate which bipartite set the node belongs to.
49
50 This function is not imported in the main namespace.
51 To use it use nx.bipartite.complete_bipartite_graph
52 """
53 G = nx.empty_graph(0, create_using)
54 if G.is_directed():
55 raise nx.NetworkXError("Directed Graph not supported")
56
57 n1, top = n1
58 n2, bottom = n2
59 if isinstance(n1, numbers.Integral) and isinstance(n2, numbers.Integral):
60 bottom = [n1 + i for i in bottom]
61 G.add_nodes_from(top, bipartite=0)
62 G.add_nodes_from(bottom, bipartite=1)
63 if len(G) != len(top) + len(bottom):
64 raise nx.NetworkXError("Inputs n1 and n2 must contain distinct nodes")
65 G.add_edges_from((u, v) for u in top for v in bottom)
66 G.graph["name"] = f"complete_bipartite_graph({len(top)}, {len(bottom)})"
67 return G
68
69
70@py_random_state(3)
71@nx._dispatchable(name="bipartite_configuration_model", graphs=None, returns_graph=True)
72def configuration_model(aseq, bseq, create_using=None, seed=None):
73 """Returns a random bipartite graph from two given degree sequences.
74
75 Parameters
76 ----------
77 aseq : list
78 Degree sequence for node set A.
79 bseq : list
80 Degree sequence for node set B.
81 create_using : NetworkX graph instance, optional
82 Return graph of this type.
83 seed : integer, random_state, or None (default)
84 Indicator of random number generation state.
85 See :ref:`Randomness<randomness>`.
86
87 The graph is composed of two partitions. Set A has nodes 0 to
88 (len(aseq) - 1) and set B has nodes len(aseq) to (len(bseq) - 1).
89 Nodes from set A are connected to nodes in set B by choosing
90 randomly from the possible free stubs, one in A and one in B.
91
92 Notes
93 -----
94 The sum of the two sequences must be equal: sum(aseq)=sum(bseq)
95 If no graph type is specified use MultiGraph with parallel edges.
96 If you want a graph with no parallel edges use create_using=Graph()
97 but then the resulting degree sequences might not be exact.
98
99 The nodes are assigned the attribute 'bipartite' with the value 0 or 1
100 to indicate which bipartite set the node belongs to.
101
102 This function is not imported in the main namespace.
103 To use it use nx.bipartite.configuration_model
104 """
105 G = nx.empty_graph(0, create_using, default=nx.MultiGraph)
106 if G.is_directed():
107 raise nx.NetworkXError("Directed Graph not supported")
108
109 # length and sum of each sequence
110 lena = len(aseq)
111 lenb = len(bseq)
112 suma = sum(aseq)
113 sumb = sum(bseq)
114
115 if not suma == sumb:
116 raise nx.NetworkXError(
117 f"invalid degree sequences, sum(aseq)!=sum(bseq),{suma},{sumb}"
118 )
119
120 G = _add_nodes_with_bipartite_label(G, lena, lenb)
121
122 if len(aseq) == 0 or max(aseq) == 0:
123 return G # done if no edges
124
125 # build lists of degree-repeated vertex numbers
126 stubs = [[v] * aseq[v] for v in range(lena)]
127 astubs = [x for subseq in stubs for x in subseq]
128
129 stubs = [[v] * bseq[v - lena] for v in range(lena, lena + lenb)]
130 bstubs = [x for subseq in stubs for x in subseq]
131
132 # shuffle lists
133 seed.shuffle(astubs)
134 seed.shuffle(bstubs)
135
136 G.add_edges_from([astubs[i], bstubs[i]] for i in range(suma))
137
138 G.name = "bipartite_configuration_model"
139 return G
140
141
142@nx._dispatchable(name="bipartite_havel_hakimi_graph", graphs=None, returns_graph=True)
143def havel_hakimi_graph(aseq, bseq, create_using=None):
144 """Returns a bipartite graph from two given degree sequences using a
145 Havel-Hakimi style construction.
146
147 The graph is composed of two partitions. Set A has nodes 0 to
148 (len(aseq) - 1) and set B has nodes len(aseq) to (len(bseq) - 1).
149 Nodes from the set A are connected to nodes in the set B by
150 connecting the highest degree nodes in set A to the highest degree
151 nodes in set B until all stubs are connected.
152
153 Parameters
154 ----------
155 aseq : list
156 Degree sequence for node set A.
157 bseq : list
158 Degree sequence for node set B.
159 create_using : NetworkX graph instance, optional
160 Return graph of this type.
161
162 Notes
163 -----
164 The sum of the two sequences must be equal: sum(aseq)=sum(bseq)
165 If no graph type is specified use MultiGraph with parallel edges.
166 If you want a graph with no parallel edges use create_using=Graph()
167 but then the resulting degree sequences might not be exact.
168
169 The nodes are assigned the attribute 'bipartite' with the value 0 or 1
170 to indicate which bipartite set the node belongs to.
171
172 This function is not imported in the main namespace.
173 To use it use nx.bipartite.havel_hakimi_graph
174 """
175 G = nx.empty_graph(0, create_using, default=nx.MultiGraph)
176 if G.is_directed():
177 raise nx.NetworkXError("Directed Graph not supported")
178
179 # length of the each sequence
180 naseq = len(aseq)
181 nbseq = len(bseq)
182
183 suma = sum(aseq)
184 sumb = sum(bseq)
185
186 if not suma == sumb:
187 raise nx.NetworkXError(
188 f"invalid degree sequences, sum(aseq)!=sum(bseq),{suma},{sumb}"
189 )
190
191 G = _add_nodes_with_bipartite_label(G, naseq, nbseq)
192
193 if len(aseq) == 0 or max(aseq) == 0:
194 return G # done if no edges
195
196 # build list of degree-repeated vertex numbers
197 astubs = [[aseq[v], v] for v in range(naseq)]
198 bstubs = [[bseq[v - naseq], v] for v in range(naseq, naseq + nbseq)]
199 astubs.sort()
200 while astubs:
201 (degree, u) = astubs.pop() # take of largest degree node in the a set
202 if degree == 0:
203 break # done, all are zero
204 # connect the source to largest degree nodes in the b set
205 bstubs.sort()
206 for target in bstubs[-degree:]:
207 v = target[1]
208 G.add_edge(u, v)
209 target[0] -= 1 # note this updates bstubs too.
210 if target[0] == 0:
211 bstubs.remove(target)
212
213 G.name = "bipartite_havel_hakimi_graph"
214 return G
215
216
217@nx._dispatchable(graphs=None, returns_graph=True)
218def reverse_havel_hakimi_graph(aseq, bseq, create_using=None):
219 """Returns a bipartite graph from two given degree sequences using a
220 Havel-Hakimi style construction.
221
222 The graph is composed of two partitions. Set A has nodes 0 to
223 (len(aseq) - 1) and set B has nodes len(aseq) to (len(bseq) - 1).
224 Nodes from set A are connected to nodes in the set B by connecting
225 the highest degree nodes in set A to the lowest degree nodes in
226 set B until all stubs are connected.
227
228 Parameters
229 ----------
230 aseq : list
231 Degree sequence for node set A.
232 bseq : list
233 Degree sequence for node set B.
234 create_using : NetworkX graph instance, optional
235 Return graph of this type.
236
237 Notes
238 -----
239 The sum of the two sequences must be equal: sum(aseq)=sum(bseq)
240 If no graph type is specified use MultiGraph with parallel edges.
241 If you want a graph with no parallel edges use create_using=Graph()
242 but then the resulting degree sequences might not be exact.
243
244 The nodes are assigned the attribute 'bipartite' with the value 0 or 1
245 to indicate which bipartite set the node belongs to.
246
247 This function is not imported in the main namespace.
248 To use it use nx.bipartite.reverse_havel_hakimi_graph
249 """
250 G = nx.empty_graph(0, create_using, default=nx.MultiGraph)
251 if G.is_directed():
252 raise nx.NetworkXError("Directed Graph not supported")
253
254 # length of the each sequence
255 lena = len(aseq)
256 lenb = len(bseq)
257 suma = sum(aseq)
258 sumb = sum(bseq)
259
260 if not suma == sumb:
261 raise nx.NetworkXError(
262 f"invalid degree sequences, sum(aseq)!=sum(bseq),{suma},{sumb}"
263 )
264
265 G = _add_nodes_with_bipartite_label(G, lena, lenb)
266
267 if len(aseq) == 0 or max(aseq) == 0:
268 return G # done if no edges
269
270 # build list of degree-repeated vertex numbers
271 astubs = [[aseq[v], v] for v in range(lena)]
272 bstubs = [[bseq[v - lena], v] for v in range(lena, lena + lenb)]
273 astubs.sort()
274 bstubs.sort()
275 while astubs:
276 (degree, u) = astubs.pop() # take of largest degree node in the a set
277 if degree == 0:
278 break # done, all are zero
279 # connect the source to the smallest degree nodes in the b set
280 for target in bstubs[0:degree]:
281 v = target[1]
282 G.add_edge(u, v)
283 target[0] -= 1 # note this updates bstubs too.
284 if target[0] == 0:
285 bstubs.remove(target)
286
287 G.name = "bipartite_reverse_havel_hakimi_graph"
288 return G
289
290
291@nx._dispatchable(graphs=None, returns_graph=True)
292def alternating_havel_hakimi_graph(aseq, bseq, create_using=None):
293 """Returns a bipartite graph from two given degree sequences using
294 an alternating Havel-Hakimi style construction.
295
296 The graph is composed of two partitions. Set A has nodes 0 to
297 (len(aseq) - 1) and set B has nodes len(aseq) to (len(bseq) - 1).
298 Nodes from the set A are connected to nodes in the set B by
299 connecting the highest degree nodes in set A to alternatively the
300 highest and the lowest degree nodes in set B until all stubs are
301 connected.
302
303 Parameters
304 ----------
305 aseq : list
306 Degree sequence for node set A.
307 bseq : list
308 Degree sequence for node set B.
309 create_using : NetworkX graph instance, optional
310 Return graph of this type.
311
312 Notes
313 -----
314 The sum of the two sequences must be equal: sum(aseq)=sum(bseq)
315 If no graph type is specified use MultiGraph with parallel edges.
316 If you want a graph with no parallel edges use create_using=Graph()
317 but then the resulting degree sequences might not be exact.
318
319 The nodes are assigned the attribute 'bipartite' with the value 0 or 1
320 to indicate which bipartite set the node belongs to.
321
322 This function is not imported in the main namespace.
323 To use it use nx.bipartite.alternating_havel_hakimi_graph
324 """
325 G = nx.empty_graph(0, create_using, default=nx.MultiGraph)
326 if G.is_directed():
327 raise nx.NetworkXError("Directed Graph not supported")
328
329 # length of the each sequence
330 naseq = len(aseq)
331 nbseq = len(bseq)
332 suma = sum(aseq)
333 sumb = sum(bseq)
334
335 if not suma == sumb:
336 raise nx.NetworkXError(
337 f"invalid degree sequences, sum(aseq)!=sum(bseq),{suma},{sumb}"
338 )
339
340 G = _add_nodes_with_bipartite_label(G, naseq, nbseq)
341
342 if len(aseq) == 0 or max(aseq) == 0:
343 return G # done if no edges
344 # build list of degree-repeated vertex numbers
345 astubs = [[aseq[v], v] for v in range(naseq)]
346 bstubs = [[bseq[v - naseq], v] for v in range(naseq, naseq + nbseq)]
347 while astubs:
348 astubs.sort()
349 (degree, u) = astubs.pop() # take of largest degree node in the a set
350 if degree == 0:
351 break # done, all are zero
352 bstubs.sort()
353 small = bstubs[0 : degree // 2] # add these low degree targets
354 large = bstubs[(-degree + degree // 2) :] # now high degree targets
355 stubs = [x for z in zip(large, small) for x in z] # combine, sorry
356 if len(stubs) < len(small) + len(large): # check for zip truncation
357 stubs.append(large.pop())
358 for target in stubs:
359 v = target[1]
360 G.add_edge(u, v)
361 target[0] -= 1 # note this updates bstubs too.
362 if target[0] == 0:
363 bstubs.remove(target)
364
365 G.name = "bipartite_alternating_havel_hakimi_graph"
366 return G
367
368
369@py_random_state(3)
370@nx._dispatchable(graphs=None, returns_graph=True)
371def preferential_attachment_graph(aseq, p, create_using=None, seed=None):
372 """Create a bipartite graph with a preferential attachment model from
373 a given single degree sequence.
374
375 The graph is composed of two partitions. Set A has nodes 0 to
376 (len(aseq) - 1) and set B has nodes starting with node len(aseq).
377 The number of nodes in set B is random.
378
379 Parameters
380 ----------
381 aseq : list
382 Degree sequence for node set A.
383 p : float
384 Probability that a new bottom node is added.
385 create_using : NetworkX graph instance, optional
386 Return graph of this type.
387 seed : integer, random_state, or None (default)
388 Indicator of random number generation state.
389 See :ref:`Randomness<randomness>`.
390
391 References
392 ----------
393 .. [1] Guillaume, J.L. and Latapy, M.,
394 Bipartite graphs as models of complex networks.
395 Physica A: Statistical Mechanics and its Applications,
396 2006, 371(2), pp.795-813.
397 .. [2] Jean-Loup Guillaume and Matthieu Latapy,
398 Bipartite structure of all complex networks,
399 Inf. Process. Lett. 90, 2004, pg. 215-221
400 https://doi.org/10.1016/j.ipl.2004.03.007
401
402 Notes
403 -----
404 The nodes are assigned the attribute 'bipartite' with the value 0 or 1
405 to indicate which bipartite set the node belongs to.
406
407 This function is not imported in the main namespace.
408 To use it use nx.bipartite.preferential_attachment_graph
409 """
410 G = nx.empty_graph(0, create_using, default=nx.MultiGraph)
411 if G.is_directed():
412 raise nx.NetworkXError("Directed Graph not supported")
413
414 if p > 1:
415 raise nx.NetworkXError(f"probability {p} > 1")
416
417 naseq = len(aseq)
418 G = _add_nodes_with_bipartite_label(G, naseq, 0)
419 vv = [[v] * aseq[v] for v in range(naseq)]
420 while vv:
421 while vv[0]:
422 source = vv[0][0]
423 vv[0].remove(source)
424 if seed.random() < p or len(G) == naseq:
425 target = len(G)
426 G.add_node(target, bipartite=1)
427 G.add_edge(source, target)
428 else:
429 bb = [[b] * G.degree(b) for b in range(naseq, len(G))]
430 # flatten the list of lists into a list.
431 bbstubs = reduce(lambda x, y: x + y, bb)
432 # choose preferentially a bottom node.
433 target = seed.choice(bbstubs)
434 G.add_node(target, bipartite=1)
435 G.add_edge(source, target)
436 vv.remove(vv[0])
437 G.name = "bipartite_preferential_attachment_model"
438 return G
439
440
441@py_random_state(3)
442@nx._dispatchable(graphs=None, returns_graph=True)
443def random_graph(n, m, p, seed=None, directed=False):
444 """Returns a bipartite random graph.
445
446 This is a bipartite version of the binomial (Erdős-Rényi) graph.
447 The graph is composed of two partitions. Set A has nodes 0 to
448 (n - 1) and set B has nodes n to (n + m - 1).
449
450 Parameters
451 ----------
452 n : int
453 The number of nodes in the first bipartite set.
454 m : int
455 The number of nodes in the second bipartite set.
456 p : float
457 Probability for edge creation.
458 seed : integer, random_state, or None (default)
459 Indicator of random number generation state.
460 See :ref:`Randomness<randomness>`.
461 directed : bool, optional (default=False)
462 If True return a directed graph
463
464 Notes
465 -----
466 The bipartite random graph algorithm chooses each of the n*m (undirected)
467 or 2*nm (directed) possible edges with probability p.
468
469 This algorithm is $O(n+m)$ where $m$ is the expected number of edges.
470
471 The nodes are assigned the attribute 'bipartite' with the value 0 or 1
472 to indicate which bipartite set the node belongs to.
473
474 This function is not imported in the main namespace.
475 To use it use nx.bipartite.random_graph
476
477 See Also
478 --------
479 gnp_random_graph, configuration_model
480
481 References
482 ----------
483 .. [1] Vladimir Batagelj and Ulrik Brandes,
484 "Efficient generation of large random networks",
485 Phys. Rev. E, 71, 036113, 2005.
486 """
487 G = nx.Graph()
488 G = _add_nodes_with_bipartite_label(G, n, m)
489 if directed:
490 G = nx.DiGraph(G)
491 G.name = f"fast_gnp_random_graph({n},{m},{p})"
492
493 if p <= 0:
494 return G
495 if p >= 1:
496 return nx.complete_bipartite_graph(n, m)
497
498 lp = math.log(1.0 - p)
499
500 v = 0
501 w = -1
502 while v < n:
503 lr = math.log(1.0 - seed.random())
504 w = w + 1 + int(lr / lp)
505 while w >= m and v < n:
506 w = w - m
507 v = v + 1
508 if v < n:
509 G.add_edge(v, n + w)
510
511 if directed:
512 # use the same algorithm to
513 # add edges from the "m" to "n" set
514 v = 0
515 w = -1
516 while v < n:
517 lr = math.log(1.0 - seed.random())
518 w = w + 1 + int(lr / lp)
519 while w >= m and v < n:
520 w = w - m
521 v = v + 1
522 if v < n:
523 G.add_edge(n + w, v)
524
525 return G
526
527
528@py_random_state(3)
529@nx._dispatchable(graphs=None, returns_graph=True)
530def gnmk_random_graph(n, m, k, seed=None, directed=False):
531 """Returns a random bipartite graph G_{n,m,k}.
532
533 Produces a bipartite graph chosen randomly out of the set of all graphs
534 with n top nodes, m bottom nodes, and k edges.
535 The graph is composed of two sets of nodes.
536 Set A has nodes 0 to (n - 1) and set B has nodes n to (n + m - 1).
537
538 Parameters
539 ----------
540 n : int
541 The number of nodes in the first bipartite set.
542 m : int
543 The number of nodes in the second bipartite set.
544 k : int
545 The number of edges
546 seed : integer, random_state, or None (default)
547 Indicator of random number generation state.
548 See :ref:`Randomness<randomness>`.
549 directed : bool, optional (default=False)
550 If True return a directed graph
551
552 Examples
553 --------
554 >>> G = nx.bipartite.gnmk_random_graph(10, 20, 50)
555
556 See Also
557 --------
558 gnm_random_graph
559
560 Notes
561 -----
562 If k > m * n then a complete bipartite graph is returned.
563
564 This graph is a bipartite version of the `G_{nm}` random graph model.
565
566 The nodes are assigned the attribute 'bipartite' with the value 0 or 1
567 to indicate which bipartite set the node belongs to.
568
569 This function is not imported in the main namespace.
570 To use it use nx.bipartite.gnmk_random_graph
571 """
572 G = nx.Graph()
573 G = _add_nodes_with_bipartite_label(G, n, m)
574 if directed:
575 G = nx.DiGraph(G)
576 G.name = f"bipartite_gnm_random_graph({n},{m},{k})"
577 if n == 1 or m == 1:
578 return G
579 max_edges = n * m # max_edges for bipartite networks
580 if k >= max_edges: # Maybe we should raise an exception here
581 return nx.complete_bipartite_graph(n, m, create_using=G)
582
583 top = [n for n, d in G.nodes(data=True) if d["bipartite"] == 0]
584 bottom = list(set(G) - set(top))
585 edge_count = 0
586 while edge_count < k:
587 # generate random edge,u,v
588 u = seed.choice(top)
589 v = seed.choice(bottom)
590 if v in G[u]:
591 continue
592 else:
593 G.add_edge(u, v)
594 edge_count += 1
595 return G
596
597
598def _add_nodes_with_bipartite_label(G, lena, lenb):
599 G.add_nodes_from(range(lena + lenb))
600 b = dict(zip(range(lena), [0] * lena))
601 b.update(dict(zip(range(lena, lena + lenb), [1] * lenb)))
602 nx.set_node_attributes(G, b, "bipartite")
603 return G