Spaces:
Running
Running
File size: 17,500 Bytes
b200bda |
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 |
"""Functions for generating line graphs."""
from collections import defaultdict
from functools import partial
from itertools import combinations
import networkx as nx
from networkx.utils import arbitrary_element
from networkx.utils.decorators import not_implemented_for
__all__ = ["line_graph", "inverse_line_graph"]
@nx._dispatch
def line_graph(G, create_using=None):
r"""Returns the line graph of the graph or digraph `G`.
The line graph of a graph `G` has a node for each edge in `G` and an
edge joining those nodes if the two edges in `G` share a common node. For
directed graphs, nodes are adjacent exactly when the edges they represent
form a directed path of length two.
The nodes of the line graph are 2-tuples of nodes in the original graph (or
3-tuples for multigraphs, with the key of the edge as the third element).
For information about self-loops and more discussion, see the **Notes**
section below.
Parameters
----------
G : graph
A NetworkX Graph, DiGraph, MultiGraph, or MultiDigraph.
create_using : NetworkX graph constructor, optional (default=nx.Graph)
Graph type to create. If graph instance, then cleared before populated.
Returns
-------
L : graph
The line graph of G.
Examples
--------
>>> G = nx.star_graph(3)
>>> L = nx.line_graph(G)
>>> print(sorted(map(sorted, L.edges()))) # makes a 3-clique, K3
[[(0, 1), (0, 2)], [(0, 1), (0, 3)], [(0, 2), (0, 3)]]
Edge attributes from `G` are not copied over as node attributes in `L`, but
attributes can be copied manually:
>>> G = nx.path_graph(4)
>>> G.add_edges_from((u, v, {"tot": u+v}) for u, v in G.edges)
>>> G.edges(data=True)
EdgeDataView([(0, 1, {'tot': 1}), (1, 2, {'tot': 3}), (2, 3, {'tot': 5})])
>>> H = nx.line_graph(G)
>>> H.add_nodes_from((node, G.edges[node]) for node in H)
>>> H.nodes(data=True)
NodeDataView({(0, 1): {'tot': 1}, (2, 3): {'tot': 5}, (1, 2): {'tot': 3}})
Notes
-----
Graph, node, and edge data are not propagated to the new graph. For
undirected graphs, the nodes in G must be sortable, otherwise the
constructed line graph may not be correct.
*Self-loops in undirected graphs*
For an undirected graph `G` without multiple edges, each edge can be
written as a set `\{u, v\}`. Its line graph `L` has the edges of `G` as
its nodes. If `x` and `y` are two nodes in `L`, then `\{x, y\}` is an edge
in `L` if and only if the intersection of `x` and `y` is nonempty. Thus,
the set of all edges is determined by the set of all pairwise intersections
of edges in `G`.
Trivially, every edge in G would have a nonzero intersection with itself,
and so every node in `L` should have a self-loop. This is not so
interesting, and the original context of line graphs was with simple
graphs, which had no self-loops or multiple edges. The line graph was also
meant to be a simple graph and thus, self-loops in `L` are not part of the
standard definition of a line graph. In a pairwise intersection matrix,
this is analogous to excluding the diagonal entries from the line graph
definition.
Self-loops and multiple edges in `G` add nodes to `L` in a natural way, and
do not require any fundamental changes to the definition. It might be
argued that the self-loops we excluded before should now be included.
However, the self-loops are still "trivial" in some sense and thus, are
usually excluded.
*Self-loops in directed graphs*
For a directed graph `G` without multiple edges, each edge can be written
as a tuple `(u, v)`. Its line graph `L` has the edges of `G` as its
nodes. If `x` and `y` are two nodes in `L`, then `(x, y)` is an edge in `L`
if and only if the tail of `x` matches the head of `y`, for example, if `x
= (a, b)` and `y = (b, c)` for some vertices `a`, `b`, and `c` in `G`.
Due to the directed nature of the edges, it is no longer the case that
every edge in `G` should have a self-loop in `L`. Now, the only time
self-loops arise is if a node in `G` itself has a self-loop. So such
self-loops are no longer "trivial" but instead, represent essential
features of the topology of `G`. For this reason, the historical
development of line digraphs is such that self-loops are included. When the
graph `G` has multiple edges, once again only superficial changes are
required to the definition.
References
----------
* Harary, Frank, and Norman, Robert Z., "Some properties of line digraphs",
Rend. Circ. Mat. Palermo, II. Ser. 9 (1960), 161--168.
* Hemminger, R. L.; Beineke, L. W. (1978), "Line graphs and line digraphs",
in Beineke, L. W.; Wilson, R. J., Selected Topics in Graph Theory,
Academic Press Inc., pp. 271--305.
"""
if G.is_directed():
L = _lg_directed(G, create_using=create_using)
else:
L = _lg_undirected(G, selfloops=False, create_using=create_using)
return L
def _lg_directed(G, create_using=None):
"""Returns the line graph L of the (multi)digraph G.
Edges in G appear as nodes in L, represented as tuples of the form (u,v)
or (u,v,key) if G is a multidigraph. A node in L corresponding to the edge
(u,v) is connected to every node corresponding to an edge (v,w).
Parameters
----------
G : digraph
A directed graph or directed multigraph.
create_using : NetworkX graph constructor, optional
Graph type to create. If graph instance, then cleared before populated.
Default is to use the same graph class as `G`.
"""
L = nx.empty_graph(0, create_using, default=G.__class__)
# Create a graph specific edge function.
get_edges = partial(G.edges, keys=True) if G.is_multigraph() else G.edges
for from_node in get_edges():
# from_node is: (u,v) or (u,v,key)
L.add_node(from_node)
for to_node in get_edges(from_node[1]):
L.add_edge(from_node, to_node)
return L
def _lg_undirected(G, selfloops=False, create_using=None):
"""Returns the line graph L of the (multi)graph G.
Edges in G appear as nodes in L, represented as sorted tuples of the form
(u,v), or (u,v,key) if G is a multigraph. A node in L corresponding to
the edge {u,v} is connected to every node corresponding to an edge that
involves u or v.
Parameters
----------
G : graph
An undirected graph or multigraph.
selfloops : bool
If `True`, then self-loops are included in the line graph. If `False`,
they are excluded.
create_using : NetworkX graph constructor, optional (default=nx.Graph)
Graph type to create. If graph instance, then cleared before populated.
Notes
-----
The standard algorithm for line graphs of undirected graphs does not
produce self-loops.
"""
L = nx.empty_graph(0, create_using, default=G.__class__)
# Graph specific functions for edges.
get_edges = partial(G.edges, keys=True) if G.is_multigraph() else G.edges
# Determine if we include self-loops or not.
shift = 0 if selfloops else 1
# Introduce numbering of nodes
node_index = {n: i for i, n in enumerate(G)}
# Lift canonical representation of nodes to edges in line graph
edge_key_function = lambda edge: (node_index[edge[0]], node_index[edge[1]])
edges = set()
for u in G:
# Label nodes as a sorted tuple of nodes in original graph.
# Decide on representation of {u, v} as (u, v) or (v, u) depending on node_index.
# -> This ensures a canonical representation and avoids comparing values of different types.
nodes = [tuple(sorted(x[:2], key=node_index.get)) + x[2:] for x in get_edges(u)]
if len(nodes) == 1:
# Then the edge will be an isolated node in L.
L.add_node(nodes[0])
# Add a clique of `nodes` to graph. To prevent double adding edges,
# especially important for multigraphs, we store the edges in
# canonical form in a set.
for i, a in enumerate(nodes):
edges.update(
[
tuple(sorted((a, b), key=edge_key_function))
for b in nodes[i + shift :]
]
)
L.add_edges_from(edges)
return L
@not_implemented_for("directed")
@not_implemented_for("multigraph")
@nx._dispatch
def inverse_line_graph(G):
"""Returns the inverse line graph of graph G.
If H is a graph, and G is the line graph of H, such that G = L(H).
Then H is the inverse line graph of G.
Not all graphs are line graphs and these do not have an inverse line graph.
In these cases this function raises a NetworkXError.
Parameters
----------
G : graph
A NetworkX Graph
Returns
-------
H : graph
The inverse line graph of G.
Raises
------
NetworkXNotImplemented
If G is directed or a multigraph
NetworkXError
If G is not a line graph
Notes
-----
This is an implementation of the Roussopoulos algorithm[1]_.
If G consists of multiple components, then the algorithm doesn't work.
You should invert every component separately:
>>> K5 = nx.complete_graph(5)
>>> P4 = nx.Graph([("a", "b"), ("b", "c"), ("c", "d")])
>>> G = nx.union(K5, P4)
>>> root_graphs = []
>>> for comp in nx.connected_components(G):
... root_graphs.append(nx.inverse_line_graph(G.subgraph(comp)))
>>> len(root_graphs)
2
References
----------
.. [1] Roussopoulos, N.D. , "A max {m, n} algorithm for determining the graph H from
its line graph G", Information Processing Letters 2, (1973), 108--112, ISSN 0020-0190,
`DOI link <https://doi.org/10.1016/0020-0190(73)90029-X>`_
"""
if G.number_of_nodes() == 0:
return nx.empty_graph(1)
elif G.number_of_nodes() == 1:
v = arbitrary_element(G)
a = (v, 0)
b = (v, 1)
H = nx.Graph([(a, b)])
return H
elif G.number_of_nodes() > 1 and G.number_of_edges() == 0:
msg = (
"inverse_line_graph() doesn't work on an edgeless graph. "
"Please use this function on each component separately."
)
raise nx.NetworkXError(msg)
if nx.number_of_selfloops(G) != 0:
msg = (
"A line graph as generated by NetworkX has no selfloops, so G has no "
"inverse line graph. Please remove the selfloops from G and try again."
)
raise nx.NetworkXError(msg)
starting_cell = _select_starting_cell(G)
P = _find_partition(G, starting_cell)
# count how many times each vertex appears in the partition set
P_count = {u: 0 for u in G.nodes}
for p in P:
for u in p:
P_count[u] += 1
if max(P_count.values()) > 2:
msg = "G is not a line graph (vertex found in more than two partition cells)"
raise nx.NetworkXError(msg)
W = tuple((u,) for u in P_count if P_count[u] == 1)
H = nx.Graph()
H.add_nodes_from(P)
H.add_nodes_from(W)
for a, b in combinations(H.nodes, 2):
if any(a_bit in b for a_bit in a):
H.add_edge(a, b)
return H
def _triangles(G, e):
"""Return list of all triangles containing edge e"""
u, v = e
if u not in G:
raise nx.NetworkXError(f"Vertex {u} not in graph")
if v not in G[u]:
raise nx.NetworkXError(f"Edge ({u}, {v}) not in graph")
triangle_list = []
for x in G[u]:
if x in G[v]:
triangle_list.append((u, v, x))
return triangle_list
def _odd_triangle(G, T):
"""Test whether T is an odd triangle in G
Parameters
----------
G : NetworkX Graph
T : 3-tuple of vertices forming triangle in G
Returns
-------
True is T is an odd triangle
False otherwise
Raises
------
NetworkXError
T is not a triangle in G
Notes
-----
An odd triangle is one in which there exists another vertex in G which is
adjacent to either exactly one or exactly all three of the vertices in the
triangle.
"""
for u in T:
if u not in G.nodes():
raise nx.NetworkXError(f"Vertex {u} not in graph")
for e in list(combinations(T, 2)):
if e[0] not in G[e[1]]:
raise nx.NetworkXError(f"Edge ({e[0]}, {e[1]}) not in graph")
T_neighbors = defaultdict(int)
for t in T:
for v in G[t]:
if v not in T:
T_neighbors[v] += 1
return any(T_neighbors[v] in [1, 3] for v in T_neighbors)
def _find_partition(G, starting_cell):
"""Find a partition of the vertices of G into cells of complete graphs
Parameters
----------
G : NetworkX Graph
starting_cell : tuple of vertices in G which form a cell
Returns
-------
List of tuples of vertices of G
Raises
------
NetworkXError
If a cell is not a complete subgraph then G is not a line graph
"""
G_partition = G.copy()
P = [starting_cell] # partition set
G_partition.remove_edges_from(list(combinations(starting_cell, 2)))
# keep list of partitioned nodes which might have an edge in G_partition
partitioned_vertices = list(starting_cell)
while G_partition.number_of_edges() > 0:
# there are still edges left and so more cells to be made
u = partitioned_vertices.pop()
deg_u = len(G_partition[u])
if deg_u != 0:
# if u still has edges then we need to find its other cell
# this other cell must be a complete subgraph or else G is
# not a line graph
new_cell = [u] + list(G_partition[u])
for u in new_cell:
for v in new_cell:
if (u != v) and (v not in G_partition[u]):
msg = (
"G is not a line graph "
"(partition cell not a complete subgraph)"
)
raise nx.NetworkXError(msg)
P.append(tuple(new_cell))
G_partition.remove_edges_from(list(combinations(new_cell, 2)))
partitioned_vertices += new_cell
return P
def _select_starting_cell(G, starting_edge=None):
"""Select a cell to initiate _find_partition
Parameters
----------
G : NetworkX Graph
starting_edge: an edge to build the starting cell from
Returns
-------
Tuple of vertices in G
Raises
------
NetworkXError
If it is determined that G is not a line graph
Notes
-----
If starting edge not specified then pick an arbitrary edge - doesn't
matter which. However, this function may call itself requiring a
specific starting edge. Note that the r, s notation for counting
triangles is the same as in the Roussopoulos paper cited above.
"""
if starting_edge is None:
e = arbitrary_element(G.edges())
else:
e = starting_edge
if e[0] not in G.nodes():
raise nx.NetworkXError(f"Vertex {e[0]} not in graph")
if e[1] not in G[e[0]]:
msg = f"starting_edge ({e[0]}, {e[1]}) is not in the Graph"
raise nx.NetworkXError(msg)
e_triangles = _triangles(G, e)
r = len(e_triangles)
if r == 0:
# there are no triangles containing e, so the starting cell is just e
starting_cell = e
elif r == 1:
# there is exactly one triangle, T, containing e. If other 2 edges
# of T belong only to this triangle then T is starting cell
T = e_triangles[0]
a, b, c = T
# ab was original edge so check the other 2 edges
ac_edges = len(_triangles(G, (a, c)))
bc_edges = len(_triangles(G, (b, c)))
if ac_edges == 1:
if bc_edges == 1:
starting_cell = T
else:
return _select_starting_cell(G, starting_edge=(b, c))
else:
return _select_starting_cell(G, starting_edge=(a, c))
else:
# r >= 2 so we need to count the number of odd triangles, s
s = 0
odd_triangles = []
for T in e_triangles:
if _odd_triangle(G, T):
s += 1
odd_triangles.append(T)
if r == 2 and s == 0:
# in this case either triangle works, so just use T
starting_cell = T
elif r - 1 <= s <= r:
# check if odd triangles containing e form complete subgraph
triangle_nodes = set()
for T in odd_triangles:
for x in T:
triangle_nodes.add(x)
for u in triangle_nodes:
for v in triangle_nodes:
if u != v and (v not in G[u]):
msg = (
"G is not a line graph (odd triangles "
"do not form complete subgraph)"
)
raise nx.NetworkXError(msg)
# otherwise then we can use this as the starting cell
starting_cell = tuple(triangle_nodes)
else:
msg = (
"G is not a line graph (incorrect number of "
"odd triangles around starting edge)"
)
raise nx.NetworkXError(msg)
return starting_cell
|