|
"""View of Graphs as SubGraph, Reverse, Directed, Undirected. |
|
|
|
In some algorithms it is convenient to temporarily morph |
|
a graph to exclude some nodes or edges. It should be better |
|
to do that via a view than to remove and then re-add. |
|
In other algorithms it is convenient to temporarily morph |
|
a graph to reverse directed edges, or treat a directed graph |
|
as undirected, etc. This module provides those graph views. |
|
|
|
The resulting views are essentially read-only graphs that |
|
report data from the original graph object. We provide an |
|
attribute G._graph which points to the underlying graph object. |
|
|
|
Note: Since graphviews look like graphs, one can end up with |
|
view-of-view-of-view chains. Be careful with chains because |
|
they become very slow with about 15 nested views. |
|
For the common simple case of node induced subgraphs created |
|
from the graph class, we short-cut the chain by returning a |
|
subgraph of the original graph directly rather than a subgraph |
|
of a subgraph. We are careful not to disrupt any edge filter in |
|
the middle subgraph. In general, determining how to short-cut |
|
the chain is tricky and much harder with restricted_views than |
|
with induced subgraphs. |
|
Often it is easiest to use .copy() to avoid chains. |
|
""" |
|
|
|
import networkx as nx |
|
from networkx.classes.coreviews import ( |
|
FilterAdjacency, |
|
FilterAtlas, |
|
FilterMultiAdjacency, |
|
UnionAdjacency, |
|
UnionMultiAdjacency, |
|
) |
|
from networkx.classes.filters import no_filter |
|
from networkx.exception import NetworkXError |
|
from networkx.utils import not_implemented_for |
|
|
|
__all__ = ["generic_graph_view", "subgraph_view", "reverse_view"] |
|
|
|
|
|
def generic_graph_view(G, create_using=None): |
|
"""Returns a read-only view of `G`. |
|
|
|
The graph `G` and its attributes are not copied but viewed through the new graph object |
|
of the same class as `G` (or of the class specified in `create_using`). |
|
|
|
Parameters |
|
---------- |
|
G : graph |
|
A directed/undirected graph/multigraph. |
|
|
|
create_using : NetworkX graph constructor, optional (default=None) |
|
Graph type to create. If graph instance, then cleared before populated. |
|
If `None`, then the appropriate Graph type is inferred from `G`. |
|
|
|
Returns |
|
------- |
|
newG : graph |
|
A view of the input graph `G` and its attributes as viewed through |
|
the `create_using` class. |
|
|
|
Raises |
|
------ |
|
NetworkXError |
|
If `G` is a multigraph (or multidigraph) but `create_using` is not, or vice versa. |
|
|
|
Notes |
|
----- |
|
The returned graph view is read-only (cannot modify the graph). |
|
Yet the view reflects any changes in `G`. The intent is to mimic dict views. |
|
|
|
Examples |
|
-------- |
|
>>> G = nx.Graph() |
|
>>> G.add_edge(1, 2, weight=0.3) |
|
>>> G.add_edge(2, 3, weight=0.5) |
|
>>> G.edges(data=True) |
|
EdgeDataView([(1, 2, {'weight': 0.3}), (2, 3, {'weight': 0.5})]) |
|
|
|
The view exposes the attributes from the original graph. |
|
|
|
>>> viewG = nx.graphviews.generic_graph_view(G) |
|
>>> viewG.edges(data=True) |
|
EdgeDataView([(1, 2, {'weight': 0.3}), (2, 3, {'weight': 0.5})]) |
|
|
|
Changes to `G` are reflected in `viewG`. |
|
|
|
>>> G.remove_edge(2, 3) |
|
>>> G.edges(data=True) |
|
EdgeDataView([(1, 2, {'weight': 0.3})]) |
|
|
|
>>> viewG.edges(data=True) |
|
EdgeDataView([(1, 2, {'weight': 0.3})]) |
|
|
|
We can change the graph type with the `create_using` parameter. |
|
|
|
>>> type(G) |
|
<class 'networkx.classes.graph.Graph'> |
|
>>> viewDG = nx.graphviews.generic_graph_view(G, create_using=nx.DiGraph) |
|
>>> type(viewDG) |
|
<class 'networkx.classes.digraph.DiGraph'> |
|
""" |
|
if create_using is None: |
|
newG = G.__class__() |
|
else: |
|
newG = nx.empty_graph(0, create_using) |
|
if G.is_multigraph() != newG.is_multigraph(): |
|
raise NetworkXError("Multigraph for G must agree with create_using") |
|
newG = nx.freeze(newG) |
|
|
|
|
|
newG._graph = G |
|
newG.graph = G.graph |
|
|
|
newG._node = G._node |
|
if newG.is_directed(): |
|
if G.is_directed(): |
|
newG._succ = G._succ |
|
newG._pred = G._pred |
|
|
|
else: |
|
newG._succ = G._adj |
|
newG._pred = G._adj |
|
|
|
elif G.is_directed(): |
|
if G.is_multigraph(): |
|
newG._adj = UnionMultiAdjacency(G._succ, G._pred) |
|
else: |
|
newG._adj = UnionAdjacency(G._succ, G._pred) |
|
else: |
|
newG._adj = G._adj |
|
return newG |
|
|
|
|
|
def subgraph_view(G, *, filter_node=no_filter, filter_edge=no_filter): |
|
"""View of `G` applying a filter on nodes and edges. |
|
|
|
`subgraph_view` provides a read-only view of the input graph that excludes |
|
nodes and edges based on the outcome of two filter functions `filter_node` |
|
and `filter_edge`. |
|
|
|
The `filter_node` function takes one argument --- the node --- and returns |
|
`True` if the node should be included in the subgraph, and `False` if it |
|
should not be included. |
|
|
|
The `filter_edge` function takes two (or three arguments if `G` is a |
|
multi-graph) --- the nodes describing an edge, plus the edge-key if |
|
parallel edges are possible --- and returns `True` if the edge should be |
|
included in the subgraph, and `False` if it should not be included. |
|
|
|
Both node and edge filter functions are called on graph elements as they |
|
are queried, meaning there is no up-front cost to creating the view. |
|
|
|
Parameters |
|
---------- |
|
G : networkx.Graph |
|
A directed/undirected graph/multigraph |
|
|
|
filter_node : callable, optional |
|
A function taking a node as input, which returns `True` if the node |
|
should appear in the view. |
|
|
|
filter_edge : callable, optional |
|
A function taking as input the two nodes describing an edge (plus the |
|
edge-key if `G` is a multi-graph), which returns `True` if the edge |
|
should appear in the view. |
|
|
|
Returns |
|
------- |
|
graph : networkx.Graph |
|
A read-only graph view of the input graph. |
|
|
|
Examples |
|
-------- |
|
>>> G = nx.path_graph(6) |
|
|
|
Filter functions operate on the node, and return `True` if the node should |
|
appear in the view: |
|
|
|
>>> def filter_node(n1): |
|
... return n1 != 5 |
|
>>> view = nx.subgraph_view(G, filter_node=filter_node) |
|
>>> view.nodes() |
|
NodeView((0, 1, 2, 3, 4)) |
|
|
|
We can use a closure pattern to filter graph elements based on additional |
|
data --- for example, filtering on edge data attached to the graph: |
|
|
|
>>> G[3][4]["cross_me"] = False |
|
>>> def filter_edge(n1, n2): |
|
... return G[n1][n2].get("cross_me", True) |
|
>>> view = nx.subgraph_view(G, filter_edge=filter_edge) |
|
>>> view.edges() |
|
EdgeView([(0, 1), (1, 2), (2, 3), (4, 5)]) |
|
|
|
>>> view = nx.subgraph_view( |
|
... G, |
|
... filter_node=filter_node, |
|
... filter_edge=filter_edge, |
|
... ) |
|
>>> view.nodes() |
|
NodeView((0, 1, 2, 3, 4)) |
|
>>> view.edges() |
|
EdgeView([(0, 1), (1, 2), (2, 3)]) |
|
""" |
|
newG = nx.freeze(G.__class__()) |
|
newG._NODE_OK = filter_node |
|
newG._EDGE_OK = filter_edge |
|
|
|
|
|
newG._graph = G |
|
newG.graph = G.graph |
|
|
|
newG._node = FilterAtlas(G._node, filter_node) |
|
if G.is_multigraph(): |
|
Adj = FilterMultiAdjacency |
|
|
|
def reverse_edge(u, v, k=None): |
|
return filter_edge(v, u, k) |
|
|
|
else: |
|
Adj = FilterAdjacency |
|
|
|
def reverse_edge(u, v, k=None): |
|
return filter_edge(v, u) |
|
|
|
if G.is_directed(): |
|
newG._succ = Adj(G._succ, filter_node, filter_edge) |
|
newG._pred = Adj(G._pred, filter_node, reverse_edge) |
|
|
|
else: |
|
newG._adj = Adj(G._adj, filter_node, filter_edge) |
|
return newG |
|
|
|
|
|
@not_implemented_for("undirected") |
|
def reverse_view(G): |
|
"""View of `G` with edge directions reversed |
|
|
|
`reverse_view` returns a read-only view of the input graph where |
|
edge directions are reversed. |
|
|
|
Identical to digraph.reverse(copy=False) |
|
|
|
Parameters |
|
---------- |
|
G : networkx.DiGraph |
|
|
|
Returns |
|
------- |
|
graph : networkx.DiGraph |
|
|
|
Examples |
|
-------- |
|
>>> G = nx.DiGraph() |
|
>>> G.add_edge(1, 2) |
|
>>> G.add_edge(2, 3) |
|
>>> G.edges() |
|
OutEdgeView([(1, 2), (2, 3)]) |
|
|
|
>>> view = nx.reverse_view(G) |
|
>>> view.edges() |
|
OutEdgeView([(2, 1), (3, 2)]) |
|
""" |
|
newG = generic_graph_view(G) |
|
newG._succ, newG._pred = G._pred, G._succ |
|
|
|
return newG |
|
|