graphviews.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. """View of Graphs as SubGraph, Reverse, Directed, Undirected.
  2. In some algorithms it is convenient to temporarily morph
  3. a graph to exclude some nodes or edges. It should be better
  4. to do that via a view than to remove and then re-add.
  5. In other algorithms it is convenient to temporarily morph
  6. a graph to reverse directed edges, or treat a directed graph
  7. as undirected, etc. This module provides those graph views.
  8. The resulting views are essentially read-only graphs that
  9. report data from the original graph object. We provide an
  10. attribute G._graph which points to the underlying graph object.
  11. Note: Since graphviews look like graphs, one can end up with
  12. view-of-view-of-view chains. Be careful with chains because
  13. they become very slow with about 15 nested views.
  14. For the common simple case of node induced subgraphs created
  15. from the graph class, we short-cut the chain by returning a
  16. subgraph of the original graph directly rather than a subgraph
  17. of a subgraph. We are careful not to disrupt any edge filter in
  18. the middle subgraph. In general, determining how to short-cut
  19. the chain is tricky and much harder with restricted_views than
  20. with induced subgraphs.
  21. Often it is easiest to use .copy() to avoid chains.
  22. """
  23. import networkx as nx
  24. from networkx.classes.coreviews import (
  25. FilterAdjacency,
  26. FilterAtlas,
  27. FilterMultiAdjacency,
  28. UnionAdjacency,
  29. UnionMultiAdjacency,
  30. )
  31. from networkx.classes.filters import no_filter
  32. from networkx.exception import NetworkXError
  33. from networkx.utils import not_implemented_for
  34. __all__ = ["generic_graph_view", "subgraph_view", "reverse_view"]
  35. def generic_graph_view(G, create_using=None):
  36. """Returns a read-only view of `G`.
  37. The graph `G` and its attributes are not copied but viewed through the new graph object
  38. of the same class as `G` (or of the class specified in `create_using`).
  39. Parameters
  40. ----------
  41. G : graph
  42. A directed/undirected graph/multigraph.
  43. create_using : NetworkX graph constructor, optional (default=None)
  44. Graph type to create. If graph instance, then cleared before populated.
  45. If `None`, then the appropriate Graph type is inferred from `G`.
  46. Returns
  47. -------
  48. newG : graph
  49. A view of the input graph `G` and its attributes as viewed through
  50. the `create_using` class.
  51. Raises
  52. ------
  53. NetworkXError
  54. If `G` is a multigraph (or multidigraph) but `create_using` is not, or vice versa.
  55. Notes
  56. -----
  57. The returned graph view is read-only (cannot modify the graph).
  58. Yet the view reflects any changes in `G`. The intent is to mimic dict views.
  59. Examples
  60. --------
  61. >>> G = nx.Graph()
  62. >>> G.add_edge(1, 2, weight=0.3)
  63. >>> G.add_edge(2, 3, weight=0.5)
  64. >>> G.edges(data=True)
  65. EdgeDataView([(1, 2, {'weight': 0.3}), (2, 3, {'weight': 0.5})])
  66. The view exposes the attributes from the original graph.
  67. >>> viewG = nx.graphviews.generic_graph_view(G)
  68. >>> viewG.edges(data=True)
  69. EdgeDataView([(1, 2, {'weight': 0.3}), (2, 3, {'weight': 0.5})])
  70. Changes to `G` are reflected in `viewG`.
  71. >>> G.remove_edge(2, 3)
  72. >>> G.edges(data=True)
  73. EdgeDataView([(1, 2, {'weight': 0.3})])
  74. >>> viewG.edges(data=True)
  75. EdgeDataView([(1, 2, {'weight': 0.3})])
  76. We can change the graph type with the `create_using` parameter.
  77. >>> type(G)
  78. <class 'networkx.classes.graph.Graph'>
  79. >>> viewDG = nx.graphviews.generic_graph_view(G, create_using=nx.DiGraph)
  80. >>> type(viewDG)
  81. <class 'networkx.classes.digraph.DiGraph'>
  82. """
  83. if create_using is None:
  84. newG = G.__class__()
  85. else:
  86. newG = nx.empty_graph(0, create_using)
  87. if G.is_multigraph() != newG.is_multigraph():
  88. raise NetworkXError("Multigraph for G must agree with create_using")
  89. newG = nx.freeze(newG)
  90. # create view by assigning attributes from G
  91. newG._graph = G
  92. newG.graph = G.graph
  93. newG._node = G._node
  94. if newG.is_directed():
  95. if G.is_directed():
  96. newG._succ = G._succ
  97. newG._pred = G._pred
  98. # newG._adj is synced with _succ
  99. else:
  100. newG._succ = G._adj
  101. newG._pred = G._adj
  102. # newG._adj is synced with _succ
  103. elif G.is_directed():
  104. if G.is_multigraph():
  105. newG._adj = UnionMultiAdjacency(G._succ, G._pred)
  106. else:
  107. newG._adj = UnionAdjacency(G._succ, G._pred)
  108. else:
  109. newG._adj = G._adj
  110. return newG
  111. def subgraph_view(G, *, filter_node=no_filter, filter_edge=no_filter):
  112. """View of `G` applying a filter on nodes and edges.
  113. `subgraph_view` provides a read-only view of the input graph that excludes
  114. nodes and edges based on the outcome of two filter functions `filter_node`
  115. and `filter_edge`.
  116. The `filter_node` function takes one argument --- the node --- and returns
  117. `True` if the node should be included in the subgraph, and `False` if it
  118. should not be included.
  119. The `filter_edge` function takes two (or three arguments if `G` is a
  120. multi-graph) --- the nodes describing an edge, plus the edge-key if
  121. parallel edges are possible --- and returns `True` if the edge should be
  122. included in the subgraph, and `False` if it should not be included.
  123. Both node and edge filter functions are called on graph elements as they
  124. are queried, meaning there is no up-front cost to creating the view.
  125. Parameters
  126. ----------
  127. G : networkx.Graph
  128. A directed/undirected graph/multigraph
  129. filter_node : callable, optional
  130. A function taking a node as input, which returns `True` if the node
  131. should appear in the view.
  132. filter_edge : callable, optional
  133. A function taking as input the two nodes describing an edge (plus the
  134. edge-key if `G` is a multi-graph), which returns `True` if the edge
  135. should appear in the view.
  136. Returns
  137. -------
  138. graph : networkx.Graph
  139. A read-only graph view of the input graph.
  140. Examples
  141. --------
  142. >>> G = nx.path_graph(6)
  143. Filter functions operate on the node, and return `True` if the node should
  144. appear in the view:
  145. >>> def filter_node(n1):
  146. ... return n1 != 5
  147. >>> view = nx.subgraph_view(G, filter_node=filter_node)
  148. >>> view.nodes()
  149. NodeView((0, 1, 2, 3, 4))
  150. We can use a closure pattern to filter graph elements based on additional
  151. data --- for example, filtering on edge data attached to the graph:
  152. >>> G[3][4]["cross_me"] = False
  153. >>> def filter_edge(n1, n2):
  154. ... return G[n1][n2].get("cross_me", True)
  155. >>> view = nx.subgraph_view(G, filter_edge=filter_edge)
  156. >>> view.edges()
  157. EdgeView([(0, 1), (1, 2), (2, 3), (4, 5)])
  158. >>> view = nx.subgraph_view(
  159. ... G,
  160. ... filter_node=filter_node,
  161. ... filter_edge=filter_edge,
  162. ... )
  163. >>> view.nodes()
  164. NodeView((0, 1, 2, 3, 4))
  165. >>> view.edges()
  166. EdgeView([(0, 1), (1, 2), (2, 3)])
  167. """
  168. newG = nx.freeze(G.__class__())
  169. newG._NODE_OK = filter_node
  170. newG._EDGE_OK = filter_edge
  171. # create view by assigning attributes from G
  172. newG._graph = G
  173. newG.graph = G.graph
  174. newG._node = FilterAtlas(G._node, filter_node)
  175. if G.is_multigraph():
  176. Adj = FilterMultiAdjacency
  177. def reverse_edge(u, v, k=None):
  178. return filter_edge(v, u, k)
  179. else:
  180. Adj = FilterAdjacency
  181. def reverse_edge(u, v, k=None):
  182. return filter_edge(v, u)
  183. if G.is_directed():
  184. newG._succ = Adj(G._succ, filter_node, filter_edge)
  185. newG._pred = Adj(G._pred, filter_node, reverse_edge)
  186. # newG._adj is synced with _succ
  187. else:
  188. newG._adj = Adj(G._adj, filter_node, filter_edge)
  189. return newG
  190. @not_implemented_for("undirected")
  191. def reverse_view(G):
  192. """View of `G` with edge directions reversed
  193. `reverse_view` returns a read-only view of the input graph where
  194. edge directions are reversed.
  195. Identical to digraph.reverse(copy=False)
  196. Parameters
  197. ----------
  198. G : networkx.DiGraph
  199. Returns
  200. -------
  201. graph : networkx.DiGraph
  202. Examples
  203. --------
  204. >>> G = nx.DiGraph()
  205. >>> G.add_edge(1, 2)
  206. >>> G.add_edge(2, 3)
  207. >>> G.edges()
  208. OutEdgeView([(1, 2), (2, 3)])
  209. >>> view = nx.reverse_view(G)
  210. >>> view.edges()
  211. OutEdgeView([(2, 1), (3, 2)])
  212. """
  213. newG = generic_graph_view(G)
  214. newG._succ, newG._pred = G._pred, G._succ
  215. # newG._adj is synced with _succ
  216. return newG