1"""Operations on many graphs."""
2
3from itertools import chain, repeat
4
5import networkx as nx
6
7__all__ = ["union_all", "compose_all", "disjoint_union_all", "intersection_all"]
8
9
10@nx._dispatchable(graphs="[graphs]", preserve_all_attrs=True, returns_graph=True)
11def union_all(graphs, rename=()):
12 """Returns the union of all graphs.
13
14 The graphs must be disjoint, otherwise an exception is raised.
15
16 Parameters
17 ----------
18 graphs : iterable
19 Iterable of NetworkX graphs
20
21 rename : iterable , optional
22 Node names of graphs can be changed by specifying the tuple
23 rename=('G-','H-') (for example). Node "u" in G is then renamed
24 "G-u" and "v" in H is renamed "H-v". Infinite generators (like itertools.count)
25 are also supported.
26
27 Returns
28 -------
29 U : a graph with the same type as the first graph in list
30
31 Raises
32 ------
33 ValueError
34 If `graphs` is an empty list.
35
36 NetworkXError
37 In case of mixed type graphs, like MultiGraph and Graph, or directed and undirected graphs.
38
39 Notes
40 -----
41 For operating on mixed type graphs, they should be converted to the same type.
42 >>> G = nx.Graph()
43 >>> H = nx.DiGraph()
44 >>> GH = union_all([nx.DiGraph(G), H])
45
46 To force a disjoint union with node relabeling, use
47 disjoint_union_all(G,H) or convert_node_labels_to integers().
48
49 Graph, edge, and node attributes are propagated to the union graph.
50 If a graph attribute is present in multiple graphs, then the value
51 from the last graph in the list with that attribute is used.
52
53 Examples
54 --------
55 >>> G1 = nx.Graph([(1, 2), (2, 3)])
56 >>> G2 = nx.Graph([(4, 5), (5, 6)])
57 >>> result_graph = nx.union_all([G1, G2])
58 >>> result_graph.nodes()
59 NodeView((1, 2, 3, 4, 5, 6))
60 >>> result_graph.edges()
61 EdgeView([(1, 2), (2, 3), (4, 5), (5, 6)])
62
63 See Also
64 --------
65 union
66 disjoint_union_all
67 """
68 R = None
69 seen_nodes = set()
70
71 # rename graph to obtain disjoint node labels
72 def add_prefix(graph, prefix):
73 if prefix is None:
74 return graph
75
76 def label(x):
77 return f"{prefix}{x}"
78
79 return nx.relabel_nodes(graph, label)
80
81 rename = chain(rename, repeat(None))
82 graphs = (add_prefix(G, name) for G, name in zip(graphs, rename))
83
84 for i, G in enumerate(graphs):
85 G_nodes_set = set(G.nodes)
86 if i == 0:
87 # Union is the same type as first graph
88 R = G.__class__()
89 elif G.is_directed() != R.is_directed():
90 raise nx.NetworkXError("All graphs must be directed or undirected.")
91 elif G.is_multigraph() != R.is_multigraph():
92 raise nx.NetworkXError("All graphs must be graphs or multigraphs.")
93 elif not seen_nodes.isdisjoint(G_nodes_set):
94 raise nx.NetworkXError(
95 "The node sets of the graphs are not disjoint.\n"
96 "Use `rename` to specify prefixes for the graphs or use\n"
97 "disjoint_union(G1, G2, ..., GN)."
98 )
99
100 seen_nodes |= G_nodes_set
101 R.graph.update(G.graph)
102 R.add_nodes_from(G.nodes(data=True))
103 R.add_edges_from(
104 G.edges(keys=True, data=True) if G.is_multigraph() else G.edges(data=True)
105 )
106
107 if R is None:
108 raise ValueError("cannot apply union_all to an empty list")
109
110 return R
111
112
113@nx._dispatchable(graphs="[graphs]", preserve_all_attrs=True, returns_graph=True)
114def disjoint_union_all(graphs):
115 """Returns the disjoint union of all graphs.
116
117 This operation forces distinct integer node labels starting with 0
118 for the first graph in the list and numbering consecutively.
119
120 Parameters
121 ----------
122 graphs : iterable
123 Iterable of NetworkX graphs
124
125 Returns
126 -------
127 U : A graph with the same type as the first graph in list
128
129 Raises
130 ------
131 ValueError
132 If `graphs` is an empty list.
133
134 NetworkXError
135 In case of mixed type graphs, like MultiGraph and Graph, or directed and undirected graphs.
136
137 Examples
138 --------
139 >>> G1 = nx.Graph([(1, 2), (2, 3)])
140 >>> G2 = nx.Graph([(4, 5), (5, 6)])
141 >>> U = nx.disjoint_union_all([G1, G2])
142 >>> list(U.nodes())
143 [0, 1, 2, 3, 4, 5]
144 >>> list(U.edges())
145 [(0, 1), (1, 2), (3, 4), (4, 5)]
146
147 Notes
148 -----
149 For operating on mixed type graphs, they should be converted to the same type.
150
151 Graph, edge, and node attributes are propagated to the union graph.
152 If a graph attribute is present in multiple graphs, then the value
153 from the last graph in the list with that attribute is used.
154 """
155
156 def yield_relabeled(graphs):
157 first_label = 0
158 for G in graphs:
159 yield nx.convert_node_labels_to_integers(G, first_label=first_label)
160 first_label += len(G)
161
162 R = union_all(yield_relabeled(graphs))
163
164 return R
165
166
167@nx._dispatchable(graphs="[graphs]", preserve_all_attrs=True, returns_graph=True)
168def compose_all(graphs):
169 """Returns the composition of all graphs.
170
171 Composition is the simple union of the node sets and edge sets.
172 The node sets of the supplied graphs need not be disjoint.
173
174 Parameters
175 ----------
176 graphs : iterable
177 Iterable of NetworkX graphs
178
179 Returns
180 -------
181 C : A graph with the same type as the first graph in list
182
183 Raises
184 ------
185 ValueError
186 If `graphs` is an empty list.
187
188 NetworkXError
189 In case of mixed type graphs, like MultiGraph and Graph, or directed and undirected graphs.
190
191 Examples
192 --------
193 >>> G1 = nx.Graph([(1, 2), (2, 3)])
194 >>> G2 = nx.Graph([(3, 4), (5, 6)])
195 >>> C = nx.compose_all([G1, G2])
196 >>> list(C.nodes())
197 [1, 2, 3, 4, 5, 6]
198 >>> list(C.edges())
199 [(1, 2), (2, 3), (3, 4), (5, 6)]
200
201 Notes
202 -----
203 For operating on mixed type graphs, they should be converted to the same type.
204
205 Graph, edge, and node attributes are propagated to the union graph.
206 If a graph attribute is present in multiple graphs, then the value
207 from the last graph in the list with that attribute is used.
208 """
209 R = None
210
211 # add graph attributes, H attributes take precedent over G attributes
212 for i, G in enumerate(graphs):
213 if i == 0:
214 # create new graph
215 R = G.__class__()
216 elif G.is_directed() != R.is_directed():
217 raise nx.NetworkXError("All graphs must be directed or undirected.")
218 elif G.is_multigraph() != R.is_multigraph():
219 raise nx.NetworkXError("All graphs must be graphs or multigraphs.")
220
221 R.graph.update(G.graph)
222 R.add_nodes_from(G.nodes(data=True))
223 R.add_edges_from(
224 G.edges(keys=True, data=True) if G.is_multigraph() else G.edges(data=True)
225 )
226
227 if R is None:
228 raise ValueError("cannot apply compose_all to an empty list")
229
230 return R
231
232
233@nx._dispatchable(graphs="[graphs]", returns_graph=True)
234def intersection_all(graphs):
235 """Returns a new graph that contains only the nodes and the edges that exist in
236 all graphs.
237
238 Parameters
239 ----------
240 graphs : iterable
241 Iterable of NetworkX graphs
242
243 Returns
244 -------
245 R : A new graph with the same type as the first graph in list
246
247 Raises
248 ------
249 ValueError
250 If `graphs` is an empty list.
251
252 NetworkXError
253 In case of mixed type graphs, like MultiGraph and Graph, or directed and undirected graphs.
254
255 Notes
256 -----
257 For operating on mixed type graphs, they should be converted to the same type.
258
259 Attributes from the graph, nodes, and edges are not copied to the new
260 graph.
261
262 The resulting graph can be updated with attributes if desired.
263 For example, code which adds the minimum attribute for each node across all
264 graphs could work::
265
266 >>> g = nx.Graph()
267 >>> g.add_node(0, capacity=4)
268 >>> g.add_node(1, capacity=3)
269 >>> g.add_edge(0, 1)
270
271 >>> h = g.copy()
272 >>> h.nodes[0]["capacity"] = 2
273
274 >>> gh = nx.intersection_all([g, h])
275
276 >>> new_node_attr = {
277 ... n: min(*(anyG.nodes[n].get("capacity", float("inf")) for anyG in [g, h]))
278 ... for n in gh
279 ... }
280 >>> nx.set_node_attributes(gh, new_node_attr, "new_capacity")
281 >>> gh.nodes(data=True)
282 NodeDataView({0: {'new_capacity': 2}, 1: {'new_capacity': 3}})
283
284 Examples
285 --------
286 >>> G1 = nx.Graph([(1, 2), (2, 3)])
287 >>> G2 = nx.Graph([(2, 3), (3, 4)])
288 >>> R = nx.intersection_all([G1, G2])
289 >>> list(R.nodes())
290 [2, 3]
291 >>> list(R.edges())
292 [(2, 3)]
293
294 """
295 R = None
296
297 for i, G in enumerate(graphs):
298 G_nodes_set = set(G.nodes)
299 G_edges_set = set(G.edges)
300 if not G.is_directed():
301 if G.is_multigraph():
302 G_edges_set.update((v, u, k) for u, v, k in list(G_edges_set))
303 else:
304 G_edges_set.update((v, u) for u, v in list(G_edges_set))
305 if i == 0:
306 # create new graph
307 R = G.__class__()
308 node_intersection = G_nodes_set
309 edge_intersection = G_edges_set
310 elif G.is_directed() != R.is_directed():
311 raise nx.NetworkXError("All graphs must be directed or undirected.")
312 elif G.is_multigraph() != R.is_multigraph():
313 raise nx.NetworkXError("All graphs must be graphs or multigraphs.")
314 else:
315 node_intersection &= G_nodes_set
316 edge_intersection &= G_edges_set
317
318 if R is None:
319 raise ValueError("cannot apply intersection_all to an empty list")
320
321 R.add_nodes_from(node_intersection)
322 R.add_edges_from(edge_intersection)
323
324 return R