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
|
"""Provides functions for computing minors of a graph."""
from itertools import chain, combinations, permutations, product
import networkx as nx
from networkx import density
from networkx.exception import NetworkXException
from networkx.utils import arbitrary_element
__all__ = [
"contracted_edge",
"contracted_nodes",
"equivalence_classes",
"identified_nodes",
"quotient_graph",
]
chaini = chain.from_iterable
def equivalence_classes(iterable, relation):
"""Returns equivalence classes of `relation` when applied to `iterable`.
The equivalence classes, or blocks, consist of objects from `iterable`
which are all equivalent. They are defined to be equivalent if the
`relation` function returns `True` when passed any two objects from that
class, and `False` otherwise. To define an equivalence relation the
function must be reflexive, symmetric and transitive.
Parameters
----------
iterable : list, tuple, or set
An iterable of elements/nodes.
relation : function
A Boolean-valued function that implements an equivalence relation
(reflexive, symmetric, transitive binary relation) on the elements
of `iterable` - it must take two elements and return `True` if
they are related, or `False` if not.
Returns
-------
set of frozensets
A set of frozensets representing the partition induced by the equivalence
relation function `relation` on the elements of `iterable`. Each
member set in the return set represents an equivalence class, or
block, of the partition.
Duplicate elements will be ignored so it makes the most sense for
`iterable` to be a :class:`set`.
Notes
-----
This function does not check that `relation` represents an equivalence
relation. You can check that your equivalence classes provide a partition
using `is_partition`.
Examples
--------
Let `X` be the set of integers from `0` to `9`, and consider an equivalence
relation `R` on `X` of congruence modulo `3`: this means that two integers
`x` and `y` in `X` are equivalent under `R` if they leave the same
remainder when divided by `3`, i.e. `(x - y) mod 3 = 0`.
The equivalence classes of this relation are `{0, 3, 6, 9}`, `{1, 4, 7}`,
`{2, 5, 8}`: `0`, `3`, `6`, `9` are all divisible by `3` and leave zero
remainder; `1`, `4`, `7` leave remainder `1`; while `2`, `5` and `8` leave
remainder `2`. We can see this by calling `equivalence_classes` with
`X` and a function implementation of `R`.
>>> X = set(range(10))
>>> def mod3(x, y):
... return (x - y) % 3 == 0
>>> equivalence_classes(X, mod3) # doctest: +SKIP
{frozenset({1, 4, 7}), frozenset({8, 2, 5}), frozenset({0, 9, 3, 6})}
"""
# For simplicity of implementation, we initialize the return value as a
# list of lists, then convert it to a set of sets at the end of the
# function.
blocks = []
# Determine the equivalence class for each element of the iterable.
for y in iterable:
# Each element y must be in *exactly one* equivalence class.
#
# Each block is guaranteed to be non-empty
for block in blocks:
x = arbitrary_element(block)
if relation(x, y):
block.append(y)
break
else:
# If the element y is not part of any known equivalence class, it
# must be in its own, so we create a new singleton equivalence
# class for it.
blocks.append([y])
return {frozenset(block) for block in blocks}
@nx._dispatchable(edge_attrs="weight", returns_graph=True)
def quotient_graph(
G,
partition,
edge_relation=None,
node_data=None,
edge_data=None,
weight="weight",
relabel=False,
create_using=None,
):
"""Returns the quotient graph of `G` under the specified equivalence
relation on nodes.
Parameters
----------
G : NetworkX graph
The graph for which to return the quotient graph with the
specified node relation.
partition : function, or dict or list of lists, tuples or sets
If a function, this function must represent an equivalence
relation on the nodes of `G`. It must take two arguments *u*
and *v* and return True exactly when *u* and *v* are in the
same equivalence class. The equivalence classes form the nodes
in the returned graph.
If a dict of lists/tuples/sets, the keys can be any meaningful
block labels, but the values must be the block lists/tuples/sets
(one list/tuple/set per block), and the blocks must form a valid
partition of the nodes of the graph. That is, each node must be
in exactly one block of the partition.
If a list of sets, the list must form a valid partition of
the nodes of the graph. That is, each node must be in exactly
one block of the partition.
edge_relation : Boolean function with two arguments
This function must represent an edge relation on the *blocks* of
the `partition` of `G`. It must take two arguments, *B* and *C*,
each one a set of nodes, and return True exactly when there should be
an edge joining block *B* to block *C* in the returned graph.
If `edge_relation` is not specified, it is assumed to be the
following relation. Block *B* is related to block *C* if and
only if some node in *B* is adjacent to some node in *C*,
according to the edge set of `G`.
node_data : function
This function takes one argument, *B*, a set of nodes in `G`,
and must return a dictionary representing the node data
attributes to set on the node representing *B* in the quotient graph.
If None, the following node attributes will be set:
* 'graph', the subgraph of the graph `G` that this block
represents,
* 'nnodes', the number of nodes in this block,
* 'nedges', the number of edges within this block,
* 'density', the density of the subgraph of `G` that this
block represents.
edge_data : function
This function takes two arguments, *B* and *C*, each one a set
of nodes, and must return a dictionary representing the edge
data attributes to set on the edge joining *B* and *C*, should
there be an edge joining *B* and *C* in the quotient graph (if
no such edge occurs in the quotient graph as determined by
`edge_relation`, then the output of this function is ignored).
If the quotient graph would be a multigraph, this function is
not applied, since the edge data from each edge in the graph
`G` appears in the edges of the quotient graph.
weight : string or None, optional (default="weight")
The name of an edge attribute that holds the numerical value
used as a weight. If None then each edge has weight 1.
relabel : bool
If True, relabel the nodes of the quotient graph to be
nonnegative integers. Otherwise, the nodes are identified with
:class:`frozenset` instances representing the blocks given in
`partition`.
create_using : NetworkX graph constructor, optional (default=nx.Graph)
Graph type to create. If graph instance, then cleared before populated.
Returns
-------
NetworkX graph
The quotient graph of `G` under the equivalence relation
specified by `partition`. If the partition were given as a
list of :class:`set` instances and `relabel` is False,
each node will be a :class:`frozenset` corresponding to the same
:class:`set`.
Raises
------
NetworkXException
If the given partition is not a valid partition of the nodes of
`G`.
Examples
--------
The quotient graph of the complete bipartite graph under the "same
neighbors" equivalence relation is `K_2`. Under this relation, two nodes
are equivalent if they are not adjacent but have the same neighbor set.
>>> G = nx.complete_bipartite_graph(2, 3)
>>> same_neighbors = lambda u, v: (u not in G[v] and v not in G[u] and G[u] == G[v])
>>> Q = nx.quotient_graph(G, same_neighbors)
>>> K2 = nx.complete_graph(2)
>>> nx.is_isomorphic(Q, K2)
True
The quotient graph of a directed graph under the "same strongly connected
component" equivalence relation is the condensation of the graph (see
:func:`condensation`). This example comes from the Wikipedia article
*`Strongly connected component`_*.
>>> G = nx.DiGraph()
>>> edges = [
... "ab",
... "be",
... "bf",
... "bc",
... "cg",
... "cd",
... "dc",
... "dh",
... "ea",
... "ef",
... "fg",
... "gf",
... "hd",
... "hf",
... ]
>>> G.add_edges_from(tuple(x) for x in edges)
>>> components = list(nx.strongly_connected_components(G))
>>> sorted(sorted(component) for component in components)
[['a', 'b', 'e'], ['c', 'd', 'h'], ['f', 'g']]
>>>
>>> C = nx.condensation(G, components)
>>> component_of = C.graph["mapping"]
>>> same_component = lambda u, v: component_of[u] == component_of[v]
>>> Q = nx.quotient_graph(G, same_component)
>>> nx.is_isomorphic(C, Q)
True
Node identification can be represented as the quotient of a graph under the
equivalence relation that places the two nodes in one block and each other
node in its own singleton block.
>>> K24 = nx.complete_bipartite_graph(2, 4)
>>> K34 = nx.complete_bipartite_graph(3, 4)
>>> C = nx.contracted_nodes(K34, 1, 2)
>>> nodes = {1, 2}
>>> is_contracted = lambda u, v: u in nodes and v in nodes
>>> Q = nx.quotient_graph(K34, is_contracted)
>>> nx.is_isomorphic(Q, C)
True
>>> nx.is_isomorphic(Q, K24)
True
The blockmodeling technique described in [1]_ can be implemented as a
quotient graph.
>>> G = nx.path_graph(6)
>>> partition = [{0, 1}, {2, 3}, {4, 5}]
>>> M = nx.quotient_graph(G, partition, relabel=True)
>>> list(M.edges())
[(0, 1), (1, 2)]
Here is the sample example but using partition as a dict of block sets.
>>> G = nx.path_graph(6)
>>> partition = {0: {0, 1}, 2: {2, 3}, 4: {4, 5}}
>>> M = nx.quotient_graph(G, partition, relabel=True)
>>> list(M.edges())
[(0, 1), (1, 2)]
Partitions can be represented in various ways:
0. a list/tuple/set of block lists/tuples/sets
1. a dict with block labels as keys and blocks lists/tuples/sets as values
2. a dict with block lists/tuples/sets as keys and block labels as values
3. a function from nodes in the original iterable to block labels
4. an equivalence relation function on the target iterable
As `quotient_graph` is designed to accept partitions represented as (0), (1) or
(4) only, the `equivalence_classes` function can be used to get the partitions
in the right form, in order to call `quotient_graph`.
.. _Strongly connected component: https://en.wikipedia.org/wiki/Strongly_connected_component
References
----------
.. [1] Patrick Doreian, Vladimir Batagelj, and Anuska Ferligoj.
*Generalized Blockmodeling*.
Cambridge University Press, 2004.
"""
# If the user provided an equivalence relation as a function to compute
# the blocks of the partition on the nodes of G induced by the
# equivalence relation.
if callable(partition):
# equivalence_classes always return partition of whole G.
partition = equivalence_classes(G, partition)
if not nx.community.is_partition(G, partition):
raise nx.NetworkXException(
"Input `partition` is not an equivalence relation for nodes of G"
)
return _quotient_graph(
G,
partition,
edge_relation,
node_data,
edge_data,
weight,
relabel,
create_using,
)
# If the partition is a dict, it is assumed to be one where the keys are
# user-defined block labels, and values are block lists, tuples or sets.
if isinstance(partition, dict):
partition = list(partition.values())
# If the user provided partition as a collection of sets. Then we
# need to check if partition covers all of G nodes. If the answer
# is 'No' then we need to prepare suitable subgraph view.
partition_nodes = set().union(*partition)
if len(partition_nodes) != len(G):
G = G.subgraph(partition_nodes)
# Each node in the graph/subgraph must be in exactly one block.
if not nx.community.is_partition(G, partition):
raise NetworkXException("each node must be in exactly one part of `partition`")
return _quotient_graph(
G,
partition,
edge_relation,
node_data,
edge_data,
weight,
relabel,
create_using,
)
def _quotient_graph(
G, partition, edge_relation, node_data, edge_data, weight, relabel, create_using
):
"""Construct the quotient graph assuming input has been checked"""
if create_using is None:
H = G.__class__()
else:
H = nx.empty_graph(0, create_using)
# By default set some basic information about the subgraph that each block
# represents on the nodes in the quotient graph.
if node_data is None:
def node_data(b):
S = G.subgraph(b)
return {
"graph": S,
"nnodes": len(S),
"nedges": S.number_of_edges(),
"density": density(S),
}
# Each block of the partition becomes a node in the quotient graph.
partition = [frozenset(b) for b in partition]
H.add_nodes_from((b, node_data(b)) for b in partition)
# By default, the edge relation is the relation defined as follows. B is
# adjacent to C if a node in B is adjacent to a node in C, according to the
# edge set of G.
#
# This is not a particularly efficient implementation of this relation:
# there are O(n^2) pairs to check and each check may require O(log n) time
# (to check set membership). This can certainly be parallelized.
if edge_relation is None:
def edge_relation(b, c):
return any(v in G[u] for u, v in product(b, c))
# By default, sum the weights of the edges joining pairs of nodes across
# blocks to get the weight of the edge joining those two blocks.
if edge_data is None:
def edge_data(b, c):
edgedata = (
d
for u, v, d in G.edges(b | c, data=True)
if (u in b and v in c) or (u in c and v in b)
)
return {"weight": sum(d.get(weight, 1) for d in edgedata)}
block_pairs = permutations(H, 2) if H.is_directed() else combinations(H, 2)
# In a multigraph, add one edge in the quotient graph for each edge
# in the original graph.
if H.is_multigraph():
edges = chaini(
(
(b, c, G.get_edge_data(u, v, default={}))
for u, v in product(b, c)
if v in G[u]
)
for b, c in block_pairs
if edge_relation(b, c)
)
# In a simple graph, apply the edge data function to each pair of
# blocks to determine the edge data attributes to apply to each edge
# in the quotient graph.
else:
edges = (
(b, c, edge_data(b, c)) for (b, c) in block_pairs if edge_relation(b, c)
)
H.add_edges_from(edges)
# If requested by the user, relabel the nodes to be integers,
# numbered in increasing order from zero in the same order as the
# iteration order of `partition`.
if relabel:
# Can't use nx.convert_node_labels_to_integers() here since we
# want the order of iteration to be the same for backward
# compatibility with the nx.blockmodel() function.
labels = {b: i for i, b in enumerate(partition)}
H = nx.relabel_nodes(H, labels)
return H
@nx._dispatchable(
preserve_all_attrs=True, mutates_input={"not copy": 4}, returns_graph=True
)
def contracted_nodes(G, u, v, self_loops=True, copy=True):
"""Returns the graph that results from contracting `u` and `v`.
Node contraction identifies the two nodes as a single node incident to any
edge that was incident to the original two nodes.
Parameters
----------
G : NetworkX graph
The graph whose nodes will be contracted.
u, v : nodes
Must be nodes in `G`.
self_loops : Boolean
If this is True, any edges joining `u` and `v` in `G` become
self-loops on the new node in the returned graph.
copy : Boolean
If this is True (default True), make a copy of
`G` and return that instead of directly changing `G`.
Returns
-------
Networkx graph
If Copy is True,
A new graph object of the same type as `G` (leaving `G` unmodified)
with `u` and `v` identified in a single node. The right node `v`
will be merged into the node `u`, so only `u` will appear in the
returned graph.
If copy is False,
Modifies `G` with `u` and `v` identified in a single node.
The right node `v` will be merged into the node `u`, so
only `u` will appear in the returned graph.
Notes
-----
For multigraphs, the edge keys for the realigned edges may
not be the same as the edge keys for the old edges. This is
natural because edge keys are unique only within each pair of nodes.
For non-multigraphs where `u` and `v` are adjacent to a third node
`w`, the edge (`v`, `w`) will be contracted into the edge (`u`,
`w`) with its attributes stored into a "contraction" attribute.
This function is also available as `identified_nodes`.
Examples
--------
Contracting two nonadjacent nodes of the cycle graph on four nodes `C_4`
yields the path graph (ignoring parallel edges):
>>> G = nx.cycle_graph(4)
>>> M = nx.contracted_nodes(G, 1, 3)
>>> P3 = nx.path_graph(3)
>>> nx.is_isomorphic(M, P3)
True
>>> G = nx.MultiGraph(P3)
>>> M = nx.contracted_nodes(G, 0, 2)
>>> M.edges
MultiEdgeView([(0, 1, 0), (0, 1, 1)])
>>> G = nx.Graph([(1, 2), (2, 2)])
>>> H = nx.contracted_nodes(G, 1, 2, self_loops=False)
>>> list(H.nodes())
[1]
>>> list(H.edges())
[(1, 1)]
In a ``MultiDiGraph`` with a self loop, the in and out edges will
be treated separately as edges, so while contracting a node which
has a self loop the contraction will add multiple edges:
>>> G = nx.MultiDiGraph([(1, 2), (2, 2)])
>>> H = nx.contracted_nodes(G, 1, 2)
>>> list(H.edges()) # edge 1->2, 2->2, 2<-2 from the original Graph G
[(1, 1), (1, 1), (1, 1)]
>>> H = nx.contracted_nodes(G, 1, 2, self_loops=False)
>>> list(H.edges()) # edge 2->2, 2<-2 from the original Graph G
[(1, 1), (1, 1)]
See Also
--------
contracted_edge
quotient_graph
"""
# Copying has significant overhead and can be disabled if needed
if copy:
H = G.copy()
else:
H = G
# edge code uses G.edges(v) instead of G.adj[v] to handle multiedges
if H.is_directed():
edges_to_remap = chain(G.in_edges(v, data=True), G.out_edges(v, data=True))
else:
edges_to_remap = G.edges(v, data=True)
# If the H=G, the generators change as H changes
# This makes the edges_to_remap independent of H
if not copy:
edges_to_remap = list(edges_to_remap)
v_data = H.nodes[v]
H.remove_node(v)
for prev_w, prev_x, d in edges_to_remap:
w = prev_w if prev_w != v else u
x = prev_x if prev_x != v else u
if ({prev_w, prev_x} == {u, v}) and not self_loops:
continue
if not H.has_edge(w, x) or G.is_multigraph():
H.add_edge(w, x, **d)
else:
if "contraction" in H.edges[(w, x)]:
H.edges[(w, x)]["contraction"][(prev_w, prev_x)] = d
else:
H.edges[(w, x)]["contraction"] = {(prev_w, prev_x): d}
if "contraction" in H.nodes[u]:
H.nodes[u]["contraction"][v] = v_data
else:
H.nodes[u]["contraction"] = {v: v_data}
return H
identified_nodes = contracted_nodes
@nx._dispatchable(
preserve_edge_attrs=True, mutates_input={"not copy": 3}, returns_graph=True
)
def contracted_edge(G, edge, self_loops=True, copy=True):
"""Returns the graph that results from contracting the specified edge.
Edge contraction identifies the two endpoints of the edge as a single node
incident to any edge that was incident to the original two nodes. A graph
that results from edge contraction is called a *minor* of the original
graph.
Parameters
----------
G : NetworkX graph
The graph whose edge will be contracted.
edge : tuple
Must be a pair of nodes in `G`.
self_loops : Boolean
If this is True, any edges (including `edge`) joining the
endpoints of `edge` in `G` become self-loops on the new node in the
returned graph.
copy : Boolean (default True)
If this is True, a the contraction will be performed on a copy of `G`,
otherwise the contraction will happen in place.
Returns
-------
Networkx graph
A new graph object of the same type as `G` (leaving `G` unmodified)
with endpoints of `edge` identified in a single node. The right node
of `edge` will be merged into the left one, so only the left one will
appear in the returned graph.
Raises
------
ValueError
If `edge` is not an edge in `G`.
Examples
--------
Attempting to contract two nonadjacent nodes yields an error:
>>> G = nx.cycle_graph(4)
>>> nx.contracted_edge(G, (1, 3))
Traceback (most recent call last):
...
ValueError: Edge (1, 3) does not exist in graph G; cannot contract it
Contracting two adjacent nodes in the cycle graph on *n* nodes yields the
cycle graph on *n - 1* nodes:
>>> C5 = nx.cycle_graph(5)
>>> C4 = nx.cycle_graph(4)
>>> M = nx.contracted_edge(C5, (0, 1), self_loops=False)
>>> nx.is_isomorphic(M, C4)
True
See also
--------
contracted_nodes
quotient_graph
"""
u, v = edge[:2]
if not G.has_edge(u, v):
raise ValueError(f"Edge {edge} does not exist in graph G; cannot contract it")
return contracted_nodes(G, u, v, self_loops=self_loops, copy=copy)
|