I am done

This commit is contained in:
2024-10-30 22:14:35 +01:00
parent 720dc28c09
commit 40e2a747cf
36901 changed files with 5011519 additions and 0 deletions

View File

@ -0,0 +1,7 @@
# graph drawing and interface to graphviz
from .layout import *
from .nx_latex import *
from .nx_pylab import *
from . import nx_agraph
from . import nx_pydot

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,464 @@
"""
***************
Graphviz AGraph
***************
Interface to pygraphviz AGraph class.
Examples
--------
>>> G = nx.complete_graph(5)
>>> A = nx.nx_agraph.to_agraph(G)
>>> H = nx.nx_agraph.from_agraph(A)
See Also
--------
- Pygraphviz: http://pygraphviz.github.io/
- Graphviz: https://www.graphviz.org
- DOT Language: http://www.graphviz.org/doc/info/lang.html
"""
import os
import tempfile
import networkx as nx
__all__ = [
"from_agraph",
"to_agraph",
"write_dot",
"read_dot",
"graphviz_layout",
"pygraphviz_layout",
"view_pygraphviz",
]
@nx._dispatchable(graphs=None, returns_graph=True)
def from_agraph(A, create_using=None):
"""Returns a NetworkX Graph or DiGraph from a PyGraphviz graph.
Parameters
----------
A : PyGraphviz AGraph
A graph created with PyGraphviz
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 `A`.
Examples
--------
>>> K5 = nx.complete_graph(5)
>>> A = nx.nx_agraph.to_agraph(K5)
>>> G = nx.nx_agraph.from_agraph(A)
Notes
-----
The Graph G will have a dictionary G.graph_attr containing
the default graphviz attributes for graphs, nodes and edges.
Default node attributes will be in the dictionary G.node_attr
which is keyed by node.
Edge attributes will be returned as edge data in G. With
edge_attr=False the edge data will be the Graphviz edge weight
attribute or the value 1 if no edge weight attribute is found.
"""
if create_using is None:
if A.is_directed():
if A.is_strict():
create_using = nx.DiGraph
else:
create_using = nx.MultiDiGraph
else:
if A.is_strict():
create_using = nx.Graph
else:
create_using = nx.MultiGraph
# assign defaults
N = nx.empty_graph(0, create_using)
if A.name is not None:
N.name = A.name
# add graph attributes
N.graph.update(A.graph_attr)
# add nodes, attributes to N.node_attr
for n in A.nodes():
str_attr = {str(k): v for k, v in n.attr.items()}
N.add_node(str(n), **str_attr)
# add edges, assign edge data as dictionary of attributes
for e in A.edges():
u, v = str(e[0]), str(e[1])
attr = dict(e.attr)
str_attr = {str(k): v for k, v in attr.items()}
if not N.is_multigraph():
if e.name is not None:
str_attr["key"] = e.name
N.add_edge(u, v, **str_attr)
else:
N.add_edge(u, v, key=e.name, **str_attr)
# add default attributes for graph, nodes, and edges
# hang them on N.graph_attr
N.graph["graph"] = dict(A.graph_attr)
N.graph["node"] = dict(A.node_attr)
N.graph["edge"] = dict(A.edge_attr)
return N
def to_agraph(N):
"""Returns a pygraphviz graph from a NetworkX graph N.
Parameters
----------
N : NetworkX graph
A graph created with NetworkX
Examples
--------
>>> K5 = nx.complete_graph(5)
>>> A = nx.nx_agraph.to_agraph(K5)
Notes
-----
If N has an dict N.graph_attr an attempt will be made first
to copy properties attached to the graph (see from_agraph)
and then updated with the calling arguments if any.
"""
try:
import pygraphviz
except ImportError as err:
raise ImportError("requires pygraphviz http://pygraphviz.github.io/") from err
directed = N.is_directed()
strict = nx.number_of_selfloops(N) == 0 and not N.is_multigraph()
A = pygraphviz.AGraph(name=N.name, strict=strict, directed=directed)
# default graph attributes
A.graph_attr.update(N.graph.get("graph", {}))
A.node_attr.update(N.graph.get("node", {}))
A.edge_attr.update(N.graph.get("edge", {}))
A.graph_attr.update(
(k, v) for k, v in N.graph.items() if k not in ("graph", "node", "edge")
)
# add nodes
for n, nodedata in N.nodes(data=True):
A.add_node(n)
# Add node data
a = A.get_node(n)
for key, val in nodedata.items():
if key == "pos":
a.attr["pos"] = f"{val[0]},{val[1]}!"
else:
a.attr[key] = str(val)
# loop over edges
if N.is_multigraph():
for u, v, key, edgedata in N.edges(data=True, keys=True):
str_edgedata = {k: str(v) for k, v in edgedata.items() if k != "key"}
A.add_edge(u, v, key=str(key))
# Add edge data
a = A.get_edge(u, v)
a.attr.update(str_edgedata)
else:
for u, v, edgedata in N.edges(data=True):
str_edgedata = {k: str(v) for k, v in edgedata.items()}
A.add_edge(u, v)
# Add edge data
a = A.get_edge(u, v)
a.attr.update(str_edgedata)
return A
def write_dot(G, path):
"""Write NetworkX graph G to Graphviz dot format on path.
Parameters
----------
G : graph
A networkx graph
path : filename
Filename or file handle to write
Notes
-----
To use a specific graph layout, call ``A.layout`` prior to `write_dot`.
Note that some graphviz layouts are not guaranteed to be deterministic,
see https://gitlab.com/graphviz/graphviz/-/issues/1767 for more info.
"""
A = to_agraph(G)
A.write(path)
A.clear()
return
@nx._dispatchable(name="agraph_read_dot", graphs=None, returns_graph=True)
def read_dot(path):
"""Returns a NetworkX graph from a dot file on path.
Parameters
----------
path : file or string
File name or file handle to read.
"""
try:
import pygraphviz
except ImportError as err:
raise ImportError(
"read_dot() requires pygraphviz http://pygraphviz.github.io/"
) from err
A = pygraphviz.AGraph(file=path)
gr = from_agraph(A)
A.clear()
return gr
def graphviz_layout(G, prog="neato", root=None, args=""):
"""Create node positions for G using Graphviz.
Parameters
----------
G : NetworkX graph
A graph created with NetworkX
prog : string
Name of Graphviz layout program
root : string, optional
Root node for twopi layout
args : string, optional
Extra arguments to Graphviz layout program
Returns
-------
Dictionary of x, y, positions keyed by node.
Examples
--------
>>> G = nx.petersen_graph()
>>> pos = nx.nx_agraph.graphviz_layout(G)
>>> pos = nx.nx_agraph.graphviz_layout(G, prog="dot")
Notes
-----
This is a wrapper for pygraphviz_layout.
Note that some graphviz layouts are not guaranteed to be deterministic,
see https://gitlab.com/graphviz/graphviz/-/issues/1767 for more info.
"""
return pygraphviz_layout(G, prog=prog, root=root, args=args)
def pygraphviz_layout(G, prog="neato", root=None, args=""):
"""Create node positions for G using Graphviz.
Parameters
----------
G : NetworkX graph
A graph created with NetworkX
prog : string
Name of Graphviz layout program
root : string, optional
Root node for twopi layout
args : string, optional
Extra arguments to Graphviz layout program
Returns
-------
node_pos : dict
Dictionary of x, y, positions keyed by node.
Examples
--------
>>> G = nx.petersen_graph()
>>> pos = nx.nx_agraph.graphviz_layout(G)
>>> pos = nx.nx_agraph.graphviz_layout(G, prog="dot")
Notes
-----
If you use complex node objects, they may have the same string
representation and GraphViz could treat them as the same node.
The layout may assign both nodes a single location. See Issue #1568
If this occurs in your case, consider relabeling the nodes just
for the layout computation using something similar to::
>>> H = nx.convert_node_labels_to_integers(G, label_attribute="node_label")
>>> H_layout = nx.nx_agraph.pygraphviz_layout(G, prog="dot")
>>> G_layout = {H.nodes[n]["node_label"]: p for n, p in H_layout.items()}
Note that some graphviz layouts are not guaranteed to be deterministic,
see https://gitlab.com/graphviz/graphviz/-/issues/1767 for more info.
"""
try:
import pygraphviz
except ImportError as err:
raise ImportError("requires pygraphviz http://pygraphviz.github.io/") from err
if root is not None:
args += f"-Groot={root}"
A = to_agraph(G)
A.layout(prog=prog, args=args)
node_pos = {}
for n in G:
node = pygraphviz.Node(A, n)
try:
xs = node.attr["pos"].split(",")
node_pos[n] = tuple(float(x) for x in xs)
except:
print("no position for node", n)
node_pos[n] = (0.0, 0.0)
return node_pos
@nx.utils.open_file(5, "w+b")
def view_pygraphviz(
G, edgelabel=None, prog="dot", args="", suffix="", path=None, show=True
):
"""Views the graph G using the specified layout algorithm.
Parameters
----------
G : NetworkX graph
The machine to draw.
edgelabel : str, callable, None
If a string, then it specifies the edge attribute to be displayed
on the edge labels. If a callable, then it is called for each
edge and it should return the string to be displayed on the edges.
The function signature of `edgelabel` should be edgelabel(data),
where `data` is the edge attribute dictionary.
prog : string
Name of Graphviz layout program.
args : str
Additional arguments to pass to the Graphviz layout program.
suffix : str
If `filename` is None, we save to a temporary file. The value of
`suffix` will appear at the tail end of the temporary filename.
path : str, None
The filename used to save the image. If None, save to a temporary
file. File formats are the same as those from pygraphviz.agraph.draw.
show : bool, default = True
Whether to display the graph with :mod:`PIL.Image.show`,
default is `True`. If `False`, the rendered graph is still available
at `path`.
Returns
-------
path : str
The filename of the generated image.
A : PyGraphviz graph
The PyGraphviz graph instance used to generate the image.
Notes
-----
If this function is called in succession too quickly, sometimes the
image is not displayed. So you might consider time.sleep(.5) between
calls if you experience problems.
Note that some graphviz layouts are not guaranteed to be deterministic,
see https://gitlab.com/graphviz/graphviz/-/issues/1767 for more info.
"""
if not len(G):
raise nx.NetworkXException("An empty graph cannot be drawn.")
# If we are providing default values for graphviz, these must be set
# before any nodes or edges are added to the PyGraphviz graph object.
# The reason for this is that default values only affect incoming objects.
# If you change the default values after the objects have been added,
# then they inherit no value and are set only if explicitly set.
# to_agraph() uses these values.
attrs = ["edge", "node", "graph"]
for attr in attrs:
if attr not in G.graph:
G.graph[attr] = {}
# These are the default values.
edge_attrs = {"fontsize": "10"}
node_attrs = {
"style": "filled",
"fillcolor": "#0000FF40",
"height": "0.75",
"width": "0.75",
"shape": "circle",
}
graph_attrs = {}
def update_attrs(which, attrs):
# Update graph attributes. Return list of those which were added.
added = []
for k, v in attrs.items():
if k not in G.graph[which]:
G.graph[which][k] = v
added.append(k)
def clean_attrs(which, added):
# Remove added attributes
for attr in added:
del G.graph[which][attr]
if not G.graph[which]:
del G.graph[which]
# Update all default values
update_attrs("edge", edge_attrs)
update_attrs("node", node_attrs)
update_attrs("graph", graph_attrs)
# Convert to agraph, so we inherit default values
A = to_agraph(G)
# Remove the default values we added to the original graph.
clean_attrs("edge", edge_attrs)
clean_attrs("node", node_attrs)
clean_attrs("graph", graph_attrs)
# If the user passed in an edgelabel, we update the labels for all edges.
if edgelabel is not None:
if not callable(edgelabel):
def func(data):
return "".join([" ", str(data[edgelabel]), " "])
else:
func = edgelabel
# update all the edge labels
if G.is_multigraph():
for u, v, key, data in G.edges(keys=True, data=True):
# PyGraphviz doesn't convert the key to a string. See #339
edge = A.get_edge(u, v, str(key))
edge.attr["label"] = str(func(data))
else:
for u, v, data in G.edges(data=True):
edge = A.get_edge(u, v)
edge.attr["label"] = str(func(data))
if path is None:
ext = "png"
if suffix:
suffix = f"_{suffix}.{ext}"
else:
suffix = f".{ext}"
path = tempfile.NamedTemporaryFile(suffix=suffix, delete=False)
else:
# Assume the decorator worked and it is a file-object.
pass
# Write graph to file
A.draw(path=path, format=None, prog=prog, args=args)
path.close()
# Show graph in a new window (depends on platform configuration)
if show:
from PIL import Image
Image.open(path.name).show()
return path.name, A

View File

@ -0,0 +1,572 @@
r"""
*****
LaTeX
*****
Export NetworkX graphs in LaTeX format using the TikZ library within TeX/LaTeX.
Usually, you will want the drawing to appear in a figure environment so
you use ``to_latex(G, caption="A caption")``. If you want the raw
drawing commands without a figure environment use :func:`to_latex_raw`.
And if you want to write to a file instead of just returning the latex
code as a string, use ``write_latex(G, "filename.tex", caption="A caption")``.
To construct a figure with subfigures for each graph to be shown, provide
``to_latex`` or ``write_latex`` a list of graphs, a list of subcaptions,
and a number of rows of subfigures inside the figure.
To be able to refer to the figures or subfigures in latex using ``\\ref``,
the keyword ``latex_label`` is available for figures and `sub_labels` for
a list of labels, one for each subfigure.
We intend to eventually provide an interface to the TikZ Graph
features which include e.g. layout algorithms.
Let us know via github what you'd like to see available, or better yet
give us some code to do it, or even better make a github pull request
to add the feature.
The TikZ approach
=================
Drawing options can be stored on the graph as node/edge attributes, or
can be provided as dicts keyed by node/edge to a string of the options
for that node/edge. Similarly a label can be shown for each node/edge
by specifying the labels as graph node/edge attributes or by providing
a dict keyed by node/edge to the text to be written for that node/edge.
Options for the tikzpicture environment (e.g. "[scale=2]") can be provided
via a keyword argument. Similarly default node and edge options can be
provided through keywords arguments. The default node options are applied
to the single TikZ "path" that draws all nodes (and no edges). The default edge
options are applied to a TikZ "scope" which contains a path for each edge.
Examples
========
>>> G = nx.path_graph(3)
>>> nx.write_latex(G, "just_my_figure.tex", as_document=True)
>>> nx.write_latex(G, "my_figure.tex", caption="A path graph", latex_label="fig1")
>>> latex_code = nx.to_latex(G) # a string rather than a file
You can change many features of the nodes and edges.
>>> G = nx.path_graph(4, create_using=nx.DiGraph)
>>> pos = {n: (n, n) for n in G} # nodes set on a line
>>> G.nodes[0]["style"] = "blue"
>>> G.nodes[2]["style"] = "line width=3,draw"
>>> G.nodes[3]["label"] = "Stop"
>>> G.edges[(0, 1)]["label"] = "1st Step"
>>> G.edges[(0, 1)]["label_opts"] = "near start"
>>> G.edges[(1, 2)]["style"] = "line width=3"
>>> G.edges[(1, 2)]["label"] = "2nd Step"
>>> G.edges[(2, 3)]["style"] = "green"
>>> G.edges[(2, 3)]["label"] = "3rd Step"
>>> G.edges[(2, 3)]["label_opts"] = "near end"
>>> nx.write_latex(G, "latex_graph.tex", pos=pos, as_document=True)
Then compile the LaTeX using something like ``pdflatex latex_graph.tex``
and view the pdf file created: ``latex_graph.pdf``.
If you want **subfigures** each containing one graph, you can input a list of graphs.
>>> H1 = nx.path_graph(4)
>>> H2 = nx.complete_graph(4)
>>> H3 = nx.path_graph(8)
>>> H4 = nx.complete_graph(8)
>>> graphs = [H1, H2, H3, H4]
>>> caps = ["Path 4", "Complete graph 4", "Path 8", "Complete graph 8"]
>>> lbls = ["fig2a", "fig2b", "fig2c", "fig2d"]
>>> nx.write_latex(graphs, "subfigs.tex", n_rows=2, sub_captions=caps, sub_labels=lbls)
>>> latex_code = nx.to_latex(graphs, n_rows=2, sub_captions=caps, sub_labels=lbls)
>>> node_color = {0: "red", 1: "orange", 2: "blue", 3: "gray!90"}
>>> edge_width = {e: "line width=1.5" for e in H3.edges}
>>> pos = nx.circular_layout(H3)
>>> latex_code = nx.to_latex(H3, pos, node_options=node_color, edge_options=edge_width)
>>> print(latex_code)
\documentclass{report}
\usepackage{tikz}
\usepackage{subcaption}
<BLANKLINE>
\begin{document}
\begin{figure}
\begin{tikzpicture}
\draw
(1.0, 0.0) node[red] (0){0}
(0.707, 0.707) node[orange] (1){1}
(-0.0, 1.0) node[blue] (2){2}
(-0.707, 0.707) node[gray!90] (3){3}
(-1.0, -0.0) node (4){4}
(-0.707, -0.707) node (5){5}
(0.0, -1.0) node (6){6}
(0.707, -0.707) node (7){7};
\begin{scope}[-]
\draw[line width=1.5] (0) to (1);
\draw[line width=1.5] (1) to (2);
\draw[line width=1.5] (2) to (3);
\draw[line width=1.5] (3) to (4);
\draw[line width=1.5] (4) to (5);
\draw[line width=1.5] (5) to (6);
\draw[line width=1.5] (6) to (7);
\end{scope}
\end{tikzpicture}
\end{figure}
\end{document}
Notes
-----
If you want to change the preamble/postamble of the figure/document/subfigure
environment, use the keyword arguments: `figure_wrapper`, `document_wrapper`,
`subfigure_wrapper`. The default values are stored in private variables
e.g. ``nx.nx_layout._DOCUMENT_WRAPPER``
References
----------
TikZ: https://tikz.dev/
TikZ options details: https://tikz.dev/tikz-actions
"""
import numbers
import os
import networkx as nx
__all__ = [
"to_latex_raw",
"to_latex",
"write_latex",
]
@nx.utils.not_implemented_for("multigraph")
def to_latex_raw(
G,
pos="pos",
tikz_options="",
default_node_options="",
node_options="node_options",
node_label="label",
default_edge_options="",
edge_options="edge_options",
edge_label="label",
edge_label_options="edge_label_options",
):
"""Return a string of the LaTeX/TikZ code to draw `G`
This function produces just the code for the tikzpicture
without any enclosing environment.
Parameters
==========
G : NetworkX graph
The NetworkX graph to be drawn
pos : string or dict (default "pos")
The name of the node attribute on `G` that holds the position of each node.
Positions can be sequences of length 2 with numbers for (x,y) coordinates.
They can also be strings to denote positions in TikZ style, such as (x, y)
or (angle:radius).
If a dict, it should be keyed by node to a position.
If an empty dict, a circular layout is computed by TikZ.
tikz_options : string
The tikzpicture options description defining the options for the picture.
Often large scale options like `[scale=2]`.
default_node_options : string
The draw options for a path of nodes. Individual node options override these.
node_options : string or dict
The name of the node attribute on `G` that holds the options for each node.
Or a dict keyed by node to a string holding the options for that node.
node_label : string or dict
The name of the node attribute on `G` that holds the node label (text)
displayed for each node. If the attribute is "" or not present, the node
itself is drawn as a string. LaTeX processing such as ``"$A_1$"`` is allowed.
Or a dict keyed by node to a string holding the label for that node.
default_edge_options : string
The options for the scope drawing all edges. The default is "[-]" for
undirected graphs and "[->]" for directed graphs.
edge_options : string or dict
The name of the edge attribute on `G` that holds the options for each edge.
If the edge is a self-loop and ``"loop" not in edge_options`` the option
"loop," is added to the options for the self-loop edge. Hence you can
use "[loop above]" explicitly, but the default is "[loop]".
Or a dict keyed by edge to a string holding the options for that edge.
edge_label : string or dict
The name of the edge attribute on `G` that holds the edge label (text)
displayed for each edge. If the attribute is "" or not present, no edge
label is drawn.
Or a dict keyed by edge to a string holding the label for that edge.
edge_label_options : string or dict
The name of the edge attribute on `G` that holds the label options for
each edge. For example, "[sloped,above,blue]". The default is no options.
Or a dict keyed by edge to a string holding the label options for that edge.
Returns
=======
latex_code : string
The text string which draws the desired graph(s) when compiled by LaTeX.
See Also
========
to_latex
write_latex
"""
i4 = "\n "
i8 = "\n "
# set up position dict
# TODO allow pos to be None and use a nice TikZ default
if not isinstance(pos, dict):
pos = nx.get_node_attributes(G, pos)
if not pos:
# circular layout with radius 2
pos = {n: f"({round(360.0 * i / len(G), 3)}:2)" for i, n in enumerate(G)}
for node in G:
if node not in pos:
raise nx.NetworkXError(f"node {node} has no specified pos {pos}")
posnode = pos[node]
if not isinstance(posnode, str):
try:
posx, posy = posnode
pos[node] = f"({round(posx, 3)}, {round(posy, 3)})"
except (TypeError, ValueError):
msg = f"position pos[{node}] is not 2-tuple or a string: {posnode}"
raise nx.NetworkXError(msg)
# set up all the dicts
if not isinstance(node_options, dict):
node_options = nx.get_node_attributes(G, node_options)
if not isinstance(node_label, dict):
node_label = nx.get_node_attributes(G, node_label)
if not isinstance(edge_options, dict):
edge_options = nx.get_edge_attributes(G, edge_options)
if not isinstance(edge_label, dict):
edge_label = nx.get_edge_attributes(G, edge_label)
if not isinstance(edge_label_options, dict):
edge_label_options = nx.get_edge_attributes(G, edge_label_options)
# process default options (add brackets or not)
topts = "" if tikz_options == "" else f"[{tikz_options.strip('[]')}]"
defn = "" if default_node_options == "" else f"[{default_node_options.strip('[]')}]"
linestyle = f"{'->' if G.is_directed() else '-'}"
if default_edge_options == "":
defe = "[" + linestyle + "]"
elif "-" in default_edge_options:
defe = default_edge_options
else:
defe = f"[{linestyle},{default_edge_options.strip('[]')}]"
# Construct the string line by line
result = " \\begin{tikzpicture}" + topts
result += i4 + " \\draw" + defn
# load the nodes
for n in G:
# node options goes inside square brackets
nopts = f"[{node_options[n].strip('[]')}]" if n in node_options else ""
# node text goes inside curly brackets {}
ntext = f"{{{node_label[n]}}}" if n in node_label else f"{{{n}}}"
result += i8 + f"{pos[n]} node{nopts} ({n}){ntext}"
result += ";\n"
# load the edges
result += " \\begin{scope}" + defe
for edge in G.edges:
u, v = edge[:2]
e_opts = f"{edge_options[edge]}".strip("[]") if edge in edge_options else ""
# add loop options for selfloops if not present
if u == v and "loop" not in e_opts:
e_opts = "loop," + e_opts
e_opts = f"[{e_opts}]" if e_opts != "" else ""
# TODO -- handle bending of multiedges
els = edge_label_options[edge] if edge in edge_label_options else ""
# edge label options goes inside square brackets []
els = f"[{els.strip('[]')}]"
# edge text is drawn using the TikZ node command inside curly brackets {}
e_label = f" node{els} {{{edge_label[edge]}}}" if edge in edge_label else ""
result += i8 + f"\\draw{e_opts} ({u}) to{e_label} ({v});"
result += "\n \\end{scope}\n \\end{tikzpicture}\n"
return result
_DOC_WRAPPER_TIKZ = r"""\documentclass{{report}}
\usepackage{{tikz}}
\usepackage{{subcaption}}
\begin{{document}}
{content}
\end{{document}}"""
_FIG_WRAPPER = r"""\begin{{figure}}
{content}{caption}{label}
\end{{figure}}"""
_SUBFIG_WRAPPER = r""" \begin{{subfigure}}{{{size}\textwidth}}
{content}{caption}{label}
\end{{subfigure}}"""
def to_latex(
Gbunch,
pos="pos",
tikz_options="",
default_node_options="",
node_options="node_options",
node_label="node_label",
default_edge_options="",
edge_options="edge_options",
edge_label="edge_label",
edge_label_options="edge_label_options",
caption="",
latex_label="",
sub_captions=None,
sub_labels=None,
n_rows=1,
as_document=True,
document_wrapper=_DOC_WRAPPER_TIKZ,
figure_wrapper=_FIG_WRAPPER,
subfigure_wrapper=_SUBFIG_WRAPPER,
):
"""Return latex code to draw the graph(s) in `Gbunch`
The TikZ drawing utility in LaTeX is used to draw the graph(s).
If `Gbunch` is a graph, it is drawn in a figure environment.
If `Gbunch` is an iterable of graphs, each is drawn in a subfigure environment
within a single figure environment.
If `as_document` is True, the figure is wrapped inside a document environment
so that the resulting string is ready to be compiled by LaTeX. Otherwise,
the string is ready for inclusion in a larger tex document using ``\\include``
or ``\\input`` statements.
Parameters
==========
Gbunch : NetworkX graph or iterable of NetworkX graphs
The NetworkX graph to be drawn or an iterable of graphs
to be drawn inside subfigures of a single figure.
pos : string or list of strings
The name of the node attribute on `G` that holds the position of each node.
Positions can be sequences of length 2 with numbers for (x,y) coordinates.
They can also be strings to denote positions in TikZ style, such as (x, y)
or (angle:radius).
If a dict, it should be keyed by node to a position.
If an empty dict, a circular layout is computed by TikZ.
If you are drawing many graphs in subfigures, use a list of position dicts.
tikz_options : string
The tikzpicture options description defining the options for the picture.
Often large scale options like `[scale=2]`.
default_node_options : string
The draw options for a path of nodes. Individual node options override these.
node_options : string or dict
The name of the node attribute on `G` that holds the options for each node.
Or a dict keyed by node to a string holding the options for that node.
node_label : string or dict
The name of the node attribute on `G` that holds the node label (text)
displayed for each node. If the attribute is "" or not present, the node
itself is drawn as a string. LaTeX processing such as ``"$A_1$"`` is allowed.
Or a dict keyed by node to a string holding the label for that node.
default_edge_options : string
The options for the scope drawing all edges. The default is "[-]" for
undirected graphs and "[->]" for directed graphs.
edge_options : string or dict
The name of the edge attribute on `G` that holds the options for each edge.
If the edge is a self-loop and ``"loop" not in edge_options`` the option
"loop," is added to the options for the self-loop edge. Hence you can
use "[loop above]" explicitly, but the default is "[loop]".
Or a dict keyed by edge to a string holding the options for that edge.
edge_label : string or dict
The name of the edge attribute on `G` that holds the edge label (text)
displayed for each edge. If the attribute is "" or not present, no edge
label is drawn.
Or a dict keyed by edge to a string holding the label for that edge.
edge_label_options : string or dict
The name of the edge attribute on `G` that holds the label options for
each edge. For example, "[sloped,above,blue]". The default is no options.
Or a dict keyed by edge to a string holding the label options for that edge.
caption : string
The caption string for the figure environment
latex_label : string
The latex label used for the figure for easy referral from the main text
sub_captions : list of strings
The sub_caption string for each subfigure in the figure
sub_latex_labels : list of strings
The latex label for each subfigure in the figure
n_rows : int
The number of rows of subfigures to arrange for multiple graphs
as_document : bool
Whether to wrap the latex code in a document environment for compiling
document_wrapper : formatted text string with variable ``content``.
This text is called to evaluate the content embedded in a document
environment with a preamble setting up TikZ.
figure_wrapper : formatted text string
This text is evaluated with variables ``content``, ``caption`` and ``label``.
It wraps the content and if a caption is provided, adds the latex code for
that caption, and if a label is provided, adds the latex code for a label.
subfigure_wrapper : formatted text string
This text evaluate variables ``size``, ``content``, ``caption`` and ``label``.
It wraps the content and if a caption is provided, adds the latex code for
that caption, and if a label is provided, adds the latex code for a label.
The size is the vertical size of each row of subfigures as a fraction.
Returns
=======
latex_code : string
The text string which draws the desired graph(s) when compiled by LaTeX.
See Also
========
write_latex
to_latex_raw
"""
if hasattr(Gbunch, "adj"):
raw = to_latex_raw(
Gbunch,
pos,
tikz_options,
default_node_options,
node_options,
node_label,
default_edge_options,
edge_options,
edge_label,
edge_label_options,
)
else: # iterator of graphs
sbf = subfigure_wrapper
size = 1 / n_rows
N = len(Gbunch)
if isinstance(pos, str | dict):
pos = [pos] * N
if sub_captions is None:
sub_captions = [""] * N
if sub_labels is None:
sub_labels = [""] * N
if not (len(Gbunch) == len(pos) == len(sub_captions) == len(sub_labels)):
raise nx.NetworkXError(
"length of Gbunch, sub_captions and sub_figures must agree"
)
raw = ""
for G, pos, subcap, sublbl in zip(Gbunch, pos, sub_captions, sub_labels):
subraw = to_latex_raw(
G,
pos,
tikz_options,
default_node_options,
node_options,
node_label,
default_edge_options,
edge_options,
edge_label,
edge_label_options,
)
cap = f" \\caption{{{subcap}}}" if subcap else ""
lbl = f"\\label{{{sublbl}}}" if sublbl else ""
raw += sbf.format(size=size, content=subraw, caption=cap, label=lbl)
raw += "\n"
# put raw latex code into a figure environment and optionally into a document
raw = raw[:-1]
cap = f"\n \\caption{{{caption}}}" if caption else ""
lbl = f"\\label{{{latex_label}}}" if latex_label else ""
fig = figure_wrapper.format(content=raw, caption=cap, label=lbl)
if as_document:
return document_wrapper.format(content=fig)
return fig
@nx.utils.open_file(1, mode="w")
def write_latex(Gbunch, path, **options):
"""Write the latex code to draw the graph(s) onto `path`.
This convenience function creates the latex drawing code as a string
and writes that to a file ready to be compiled when `as_document` is True
or ready to be ``import`` ed or ``include`` ed into your main LaTeX document.
The `path` argument can be a string filename or a file handle to write to.
Parameters
----------
Gbunch : NetworkX graph or iterable of NetworkX graphs
If Gbunch is a graph, it is drawn in a figure environment.
If Gbunch is an iterable of graphs, each is drawn in a subfigure
environment within a single figure environment.
path : filename
Filename or file handle to write to
options : dict
By default, TikZ is used with options: (others are ignored)::
pos : string or dict or list
The name of the node attribute on `G` that holds the position of each node.
Positions can be sequences of length 2 with numbers for (x,y) coordinates.
They can also be strings to denote positions in TikZ style, such as (x, y)
or (angle:radius).
If a dict, it should be keyed by node to a position.
If an empty dict, a circular layout is computed by TikZ.
If you are drawing many graphs in subfigures, use a list of position dicts.
tikz_options : string
The tikzpicture options description defining the options for the picture.
Often large scale options like `[scale=2]`.
default_node_options : string
The draw options for a path of nodes. Individual node options override these.
node_options : string or dict
The name of the node attribute on `G` that holds the options for each node.
Or a dict keyed by node to a string holding the options for that node.
node_label : string or dict
The name of the node attribute on `G` that holds the node label (text)
displayed for each node. If the attribute is "" or not present, the node
itself is drawn as a string. LaTeX processing such as ``"$A_1$"`` is allowed.
Or a dict keyed by node to a string holding the label for that node.
default_edge_options : string
The options for the scope drawing all edges. The default is "[-]" for
undirected graphs and "[->]" for directed graphs.
edge_options : string or dict
The name of the edge attribute on `G` that holds the options for each edge.
If the edge is a self-loop and ``"loop" not in edge_options`` the option
"loop," is added to the options for the self-loop edge. Hence you can
use "[loop above]" explicitly, but the default is "[loop]".
Or a dict keyed by edge to a string holding the options for that edge.
edge_label : string or dict
The name of the edge attribute on `G` that holds the edge label (text)
displayed for each edge. If the attribute is "" or not present, no edge
label is drawn.
Or a dict keyed by edge to a string holding the label for that edge.
edge_label_options : string or dict
The name of the edge attribute on `G` that holds the label options for
each edge. For example, "[sloped,above,blue]". The default is no options.
Or a dict keyed by edge to a string holding the label options for that edge.
caption : string
The caption string for the figure environment
latex_label : string
The latex label used for the figure for easy referral from the main text
sub_captions : list of strings
The sub_caption string for each subfigure in the figure
sub_latex_labels : list of strings
The latex label for each subfigure in the figure
n_rows : int
The number of rows of subfigures to arrange for multiple graphs
as_document : bool
Whether to wrap the latex code in a document environment for compiling
document_wrapper : formatted text string with variable ``content``.
This text is called to evaluate the content embedded in a document
environment with a preamble setting up the TikZ syntax.
figure_wrapper : formatted text string
This text is evaluated with variables ``content``, ``caption`` and ``label``.
It wraps the content and if a caption is provided, adds the latex code for
that caption, and if a label is provided, adds the latex code for a label.
subfigure_wrapper : formatted text string
This text evaluate variables ``size``, ``content``, ``caption`` and ``label``.
It wraps the content and if a caption is provided, adds the latex code for
that caption, and if a label is provided, adds the latex code for a label.
The size is the vertical size of each row of subfigures as a fraction.
See Also
========
to_latex
"""
path.write(to_latex(Gbunch, **options))

View File

@ -0,0 +1,352 @@
"""
*****
Pydot
*****
Import and export NetworkX graphs in Graphviz dot format using pydot.
Either this module or nx_agraph can be used to interface with graphviz.
Examples
--------
>>> G = nx.complete_graph(5)
>>> PG = nx.nx_pydot.to_pydot(G)
>>> H = nx.nx_pydot.from_pydot(PG)
See Also
--------
- pydot: https://github.com/erocarrera/pydot
- Graphviz: https://www.graphviz.org
- DOT Language: http://www.graphviz.org/doc/info/lang.html
"""
from locale import getpreferredencoding
import networkx as nx
from networkx.utils import open_file
__all__ = [
"write_dot",
"read_dot",
"graphviz_layout",
"pydot_layout",
"to_pydot",
"from_pydot",
]
@open_file(1, mode="w")
def write_dot(G, path):
"""Write NetworkX graph G to Graphviz dot format on path.
Path can be a string or a file handle.
"""
P = to_pydot(G)
path.write(P.to_string())
return
@open_file(0, mode="r")
@nx._dispatchable(name="pydot_read_dot", graphs=None, returns_graph=True)
def read_dot(path):
"""Returns a NetworkX :class:`MultiGraph` or :class:`MultiDiGraph` from the
dot file with the passed path.
If this file contains multiple graphs, only the first such graph is
returned. All graphs _except_ the first are silently ignored.
Parameters
----------
path : str or file
Filename or file handle.
Returns
-------
G : MultiGraph or MultiDiGraph
A :class:`MultiGraph` or :class:`MultiDiGraph`.
Notes
-----
Use `G = nx.Graph(nx.nx_pydot.read_dot(path))` to return a :class:`Graph` instead of a
:class:`MultiGraph`.
"""
import pydot
data = path.read()
# List of one or more "pydot.Dot" instances deserialized from this file.
P_list = pydot.graph_from_dot_data(data)
# Convert only the first such instance into a NetworkX graph.
return from_pydot(P_list[0])
@nx._dispatchable(graphs=None, returns_graph=True)
def from_pydot(P):
"""Returns a NetworkX graph from a Pydot graph.
Parameters
----------
P : Pydot graph
A graph created with Pydot
Returns
-------
G : NetworkX multigraph
A MultiGraph or MultiDiGraph.
Examples
--------
>>> K5 = nx.complete_graph(5)
>>> A = nx.nx_pydot.to_pydot(K5)
>>> G = nx.nx_pydot.from_pydot(A) # return MultiGraph
# make a Graph instead of MultiGraph
>>> G = nx.Graph(nx.nx_pydot.from_pydot(A))
"""
if P.get_strict(None): # pydot bug: get_strict() shouldn't take argument
multiedges = False
else:
multiedges = True
if P.get_type() == "graph": # undirected
if multiedges:
N = nx.MultiGraph()
else:
N = nx.Graph()
else:
if multiedges:
N = nx.MultiDiGraph()
else:
N = nx.DiGraph()
# assign defaults
name = P.get_name().strip('"')
if name != "":
N.name = name
# add nodes, attributes to N.node_attr
for p in P.get_node_list():
n = p.get_name().strip('"')
if n in ("node", "graph", "edge"):
continue
N.add_node(n, **p.get_attributes())
# add edges
for e in P.get_edge_list():
u = e.get_source()
v = e.get_destination()
attr = e.get_attributes()
s = []
d = []
if isinstance(u, str):
s.append(u.strip('"'))
else:
for unodes in u["nodes"]:
s.append(unodes.strip('"'))
if isinstance(v, str):
d.append(v.strip('"'))
else:
for vnodes in v["nodes"]:
d.append(vnodes.strip('"'))
for source_node in s:
for destination_node in d:
N.add_edge(source_node, destination_node, **attr)
# add default attributes for graph, nodes, edges
pattr = P.get_attributes()
if pattr:
N.graph["graph"] = pattr
try:
N.graph["node"] = P.get_node_defaults()[0]
except (IndexError, TypeError):
pass # N.graph['node']={}
try:
N.graph["edge"] = P.get_edge_defaults()[0]
except (IndexError, TypeError):
pass # N.graph['edge']={}
return N
def to_pydot(N):
"""Returns a pydot graph from a NetworkX graph N.
Parameters
----------
N : NetworkX graph
A graph created with NetworkX
Examples
--------
>>> K5 = nx.complete_graph(5)
>>> P = nx.nx_pydot.to_pydot(K5)
Notes
-----
"""
import pydot
# set Graphviz graph type
if N.is_directed():
graph_type = "digraph"
else:
graph_type = "graph"
strict = nx.number_of_selfloops(N) == 0 and not N.is_multigraph()
name = N.name
graph_defaults = N.graph.get("graph", {})
if name == "":
P = pydot.Dot("", graph_type=graph_type, strict=strict, **graph_defaults)
else:
P = pydot.Dot(
f'"{name}"', graph_type=graph_type, strict=strict, **graph_defaults
)
try:
P.set_node_defaults(**N.graph["node"])
except KeyError:
pass
try:
P.set_edge_defaults(**N.graph["edge"])
except KeyError:
pass
for n, nodedata in N.nodes(data=True):
str_nodedata = {str(k): str(v) for k, v in nodedata.items()}
n = str(n)
p = pydot.Node(n, **str_nodedata)
P.add_node(p)
if N.is_multigraph():
for u, v, key, edgedata in N.edges(data=True, keys=True):
str_edgedata = {str(k): str(v) for k, v in edgedata.items() if k != "key"}
u, v = str(u), str(v)
edge = pydot.Edge(u, v, key=str(key), **str_edgedata)
P.add_edge(edge)
else:
for u, v, edgedata in N.edges(data=True):
str_edgedata = {str(k): str(v) for k, v in edgedata.items()}
u, v = str(u), str(v)
edge = pydot.Edge(u, v, **str_edgedata)
P.add_edge(edge)
return P
def graphviz_layout(G, prog="neato", root=None):
"""Create node positions using Pydot and Graphviz.
Returns a dictionary of positions keyed by node.
Parameters
----------
G : NetworkX Graph
The graph for which the layout is computed.
prog : string (default: 'neato')
The name of the GraphViz program to use for layout.
Options depend on GraphViz version but may include:
'dot', 'twopi', 'fdp', 'sfdp', 'circo'
root : Node from G or None (default: None)
The node of G from which to start some layout algorithms.
Returns
-------
Dictionary of (x, y) positions keyed by node.
Examples
--------
>>> G = nx.complete_graph(4)
>>> pos = nx.nx_pydot.graphviz_layout(G)
>>> pos = nx.nx_pydot.graphviz_layout(G, prog="dot")
Notes
-----
This is a wrapper for pydot_layout.
"""
return pydot_layout(G=G, prog=prog, root=root)
def pydot_layout(G, prog="neato", root=None):
"""Create node positions using :mod:`pydot` and Graphviz.
Parameters
----------
G : Graph
NetworkX graph to be laid out.
prog : string (default: 'neato')
Name of the GraphViz command to use for layout.
Options depend on GraphViz version but may include:
'dot', 'twopi', 'fdp', 'sfdp', 'circo'
root : Node from G or None (default: None)
The node of G from which to start some layout algorithms.
Returns
-------
dict
Dictionary of positions keyed by node.
Examples
--------
>>> G = nx.complete_graph(4)
>>> pos = nx.nx_pydot.pydot_layout(G)
>>> pos = nx.nx_pydot.pydot_layout(G, prog="dot")
Notes
-----
If you use complex node objects, they may have the same string
representation and GraphViz could treat them as the same node.
The layout may assign both nodes a single location. See Issue #1568
If this occurs in your case, consider relabeling the nodes just
for the layout computation using something similar to::
H = nx.convert_node_labels_to_integers(G, label_attribute="node_label")
H_layout = nx.nx_pydot.pydot_layout(H, prog="dot")
G_layout = {H.nodes[n]["node_label"]: p for n, p in H_layout.items()}
"""
import pydot
P = to_pydot(G)
if root is not None:
P.set("root", str(root))
# List of low-level bytes comprising a string in the dot language converted
# from the passed graph with the passed external GraphViz command.
D_bytes = P.create_dot(prog=prog)
# Unique string decoded from these bytes with the preferred locale encoding
D = str(D_bytes, encoding=getpreferredencoding())
if D == "": # no data returned
print(f"Graphviz layout with {prog} failed")
print()
print("To debug what happened try:")
print("P = nx.nx_pydot.to_pydot(G)")
print('P.write_dot("file.dot")')
print(f"And then run {prog} on file.dot")
return
# List of one or more "pydot.Dot" instances deserialized from this string.
Q_list = pydot.graph_from_dot_data(D)
assert len(Q_list) == 1
# The first and only such instance, as guaranteed by the above assertion.
Q = Q_list[0]
node_pos = {}
for n in G.nodes():
str_n = str(n)
node = Q.get_node(pydot.quote_id_if_necessary(str_n))
if isinstance(node, list):
node = node[0]
pos = node.get_pos()[1:-1] # strip leading and trailing double quotes
if pos is not None:
xx, yy = pos.split(",")
node_pos[n] = (float(xx), float(yy))
return node_pos

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c2qe2u5p0fqvh"
path="res://.godot/imported/test_house_with_colors.png-53cffd7d23c5dbcc0de2d795f857a24f.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://rl/Lib/site-packages/networkx/drawing/tests/baseline/test_house_with_colors.png"
dest_files=["res://.godot/imported/test_house_with_colors.png-53cffd7d23c5dbcc0de2d795f857a24f.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View File

@ -0,0 +1,241 @@
"""Unit tests for PyGraphviz interface."""
import warnings
import pytest
pygraphviz = pytest.importorskip("pygraphviz")
import networkx as nx
from networkx.utils import edges_equal, graphs_equal, nodes_equal
class TestAGraph:
def build_graph(self, G):
edges = [("A", "B"), ("A", "C"), ("A", "C"), ("B", "C"), ("A", "D")]
G.add_edges_from(edges)
G.add_node("E")
G.graph["metal"] = "bronze"
return G
def assert_equal(self, G1, G2):
assert nodes_equal(G1.nodes(), G2.nodes())
assert edges_equal(G1.edges(), G2.edges())
assert G1.graph["metal"] == G2.graph["metal"]
@pytest.mark.parametrize(
"G", (nx.Graph(), nx.DiGraph(), nx.MultiGraph(), nx.MultiDiGraph())
)
def test_agraph_roundtripping(self, G, tmp_path):
G = self.build_graph(G)
A = nx.nx_agraph.to_agraph(G)
H = nx.nx_agraph.from_agraph(A)
self.assert_equal(G, H)
fname = tmp_path / "test.dot"
nx.drawing.nx_agraph.write_dot(H, fname)
Hin = nx.nx_agraph.read_dot(fname)
self.assert_equal(H, Hin)
fname = tmp_path / "fh_test.dot"
with open(fname, "w") as fh:
nx.drawing.nx_agraph.write_dot(H, fh)
with open(fname) as fh:
Hin = nx.nx_agraph.read_dot(fh)
self.assert_equal(H, Hin)
def test_from_agraph_name(self):
G = nx.Graph(name="test")
A = nx.nx_agraph.to_agraph(G)
H = nx.nx_agraph.from_agraph(A)
assert G.name == "test"
@pytest.mark.parametrize(
"graph_class", (nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)
)
def test_from_agraph_create_using(self, graph_class):
G = nx.path_graph(3)
A = nx.nx_agraph.to_agraph(G)
H = nx.nx_agraph.from_agraph(A, create_using=graph_class)
assert isinstance(H, graph_class)
def test_from_agraph_named_edges(self):
# Create an AGraph from an existing (non-multi) Graph
G = nx.Graph()
G.add_nodes_from([0, 1])
A = nx.nx_agraph.to_agraph(G)
# Add edge (+ name, given by key) to the AGraph
A.add_edge(0, 1, key="foo")
# Verify a.name roundtrips out to 'key' in from_agraph
H = nx.nx_agraph.from_agraph(A)
assert isinstance(H, nx.Graph)
assert ("0", "1", {"key": "foo"}) in H.edges(data=True)
def test_to_agraph_with_nodedata(self):
G = nx.Graph()
G.add_node(1, color="red")
A = nx.nx_agraph.to_agraph(G)
assert dict(A.nodes()[0].attr) == {"color": "red"}
@pytest.mark.parametrize("graph_class", (nx.Graph, nx.MultiGraph))
def test_to_agraph_with_edgedata(self, graph_class):
G = graph_class()
G.add_nodes_from([0, 1])
G.add_edge(0, 1, color="yellow")
A = nx.nx_agraph.to_agraph(G)
assert dict(A.edges()[0].attr) == {"color": "yellow"}
def test_view_pygraphviz_path(self, tmp_path):
G = nx.complete_graph(3)
input_path = str(tmp_path / "graph.png")
out_path, A = nx.nx_agraph.view_pygraphviz(G, path=input_path, show=False)
assert out_path == input_path
# Ensure file is not empty
with open(input_path, "rb") as fh:
data = fh.read()
assert len(data) > 0
def test_view_pygraphviz_file_suffix(self, tmp_path):
G = nx.complete_graph(3)
path, A = nx.nx_agraph.view_pygraphviz(G, suffix=1, show=False)
assert path[-6:] == "_1.png"
def test_view_pygraphviz(self):
G = nx.Graph() # "An empty graph cannot be drawn."
pytest.raises(nx.NetworkXException, nx.nx_agraph.view_pygraphviz, G)
G = nx.barbell_graph(4, 6)
nx.nx_agraph.view_pygraphviz(G, show=False)
def test_view_pygraphviz_edgelabel(self):
G = nx.Graph()
G.add_edge(1, 2, weight=7)
G.add_edge(2, 3, weight=8)
path, A = nx.nx_agraph.view_pygraphviz(G, edgelabel="weight", show=False)
for edge in A.edges():
assert edge.attr["weight"] in ("7", "8")
def test_view_pygraphviz_callable_edgelabel(self):
G = nx.complete_graph(3)
def foo_label(data):
return "foo"
path, A = nx.nx_agraph.view_pygraphviz(G, edgelabel=foo_label, show=False)
for edge in A.edges():
assert edge.attr["label"] == "foo"
def test_view_pygraphviz_multigraph_edgelabels(self):
G = nx.MultiGraph()
G.add_edge(0, 1, key=0, name="left_fork")
G.add_edge(0, 1, key=1, name="right_fork")
path, A = nx.nx_agraph.view_pygraphviz(G, edgelabel="name", show=False)
edges = A.edges()
assert len(edges) == 2
for edge in edges:
assert edge.attr["label"].strip() in ("left_fork", "right_fork")
def test_graph_with_reserved_keywords(self):
# test attribute/keyword clash case for #1582
# node: n
# edges: u,v
G = nx.Graph()
G = self.build_graph(G)
G.nodes["E"]["n"] = "keyword"
G.edges[("A", "B")]["u"] = "keyword"
G.edges[("A", "B")]["v"] = "keyword"
A = nx.nx_agraph.to_agraph(G)
def test_view_pygraphviz_no_added_attrs_to_input(self):
G = nx.complete_graph(2)
path, A = nx.nx_agraph.view_pygraphviz(G, show=False)
assert G.graph == {}
@pytest.mark.xfail(reason="known bug in clean_attrs")
def test_view_pygraphviz_leaves_input_graph_unmodified(self):
G = nx.complete_graph(2)
# Add entries to graph dict that to_agraph handles specially
G.graph["node"] = {"width": "0.80"}
G.graph["edge"] = {"fontsize": "14"}
path, A = nx.nx_agraph.view_pygraphviz(G, show=False)
assert G.graph == {"node": {"width": "0.80"}, "edge": {"fontsize": "14"}}
def test_graph_with_AGraph_attrs(self):
G = nx.complete_graph(2)
# Add entries to graph dict that to_agraph handles specially
G.graph["node"] = {"width": "0.80"}
G.graph["edge"] = {"fontsize": "14"}
path, A = nx.nx_agraph.view_pygraphviz(G, show=False)
# Ensure user-specified values are not lost
assert dict(A.node_attr)["width"] == "0.80"
assert dict(A.edge_attr)["fontsize"] == "14"
def test_round_trip_empty_graph(self):
G = nx.Graph()
A = nx.nx_agraph.to_agraph(G)
H = nx.nx_agraph.from_agraph(A)
# assert graphs_equal(G, H)
AA = nx.nx_agraph.to_agraph(H)
HH = nx.nx_agraph.from_agraph(AA)
assert graphs_equal(H, HH)
G.graph["graph"] = {}
G.graph["node"] = {}
G.graph["edge"] = {}
assert graphs_equal(G, HH)
@pytest.mark.xfail(reason="integer->string node conversion in round trip")
def test_round_trip_integer_nodes(self):
G = nx.complete_graph(3)
A = nx.nx_agraph.to_agraph(G)
H = nx.nx_agraph.from_agraph(A)
assert graphs_equal(G, H)
def test_graphviz_alias(self):
G = self.build_graph(nx.Graph())
pos_graphviz = nx.nx_agraph.graphviz_layout(G)
pos_pygraphviz = nx.nx_agraph.pygraphviz_layout(G)
assert pos_graphviz == pos_pygraphviz
@pytest.mark.parametrize("root", range(5))
def test_pygraphviz_layout_root(self, root):
# NOTE: test depends on layout prog being deterministic
G = nx.complete_graph(5)
A = nx.nx_agraph.to_agraph(G)
# Get layout with root arg is not None
pygv_layout = nx.nx_agraph.pygraphviz_layout(G, prog="circo", root=root)
# Equivalent layout directly on AGraph
A.layout(args=f"-Groot={root}", prog="circo")
# Parse AGraph layout
a1_pos = tuple(float(v) for v in dict(A.get_node("1").attr)["pos"].split(","))
assert pygv_layout[1] == a1_pos
def test_2d_layout(self):
G = nx.Graph()
G = self.build_graph(G)
G.graph["dimen"] = 2
pos = nx.nx_agraph.pygraphviz_layout(G, prog="neato")
pos = list(pos.values())
assert len(pos) == 5
assert len(pos[0]) == 2
def test_3d_layout(self):
G = nx.Graph()
G = self.build_graph(G)
G.graph["dimen"] = 3
pos = nx.nx_agraph.pygraphviz_layout(G, prog="neato")
pos = list(pos.values())
assert len(pos) == 5
assert len(pos[0]) == 3
def test_no_warnings_raised(self):
# Test that no warnings are raised when Networkx graph
# is converted to Pygraphviz graph and 'pos'
# attribute is given
G = nx.Graph()
G.add_node(0, pos=(0, 0))
G.add_node(1, pos=(1, 1))
A = nx.nx_agraph.to_agraph(G)
with warnings.catch_warnings(record=True) as record:
A.layout()
assert len(record) == 0

View File

@ -0,0 +1,292 @@
import pytest
import networkx as nx
def test_tikz_attributes():
G = nx.path_graph(4, create_using=nx.DiGraph)
pos = {n: (n, n) for n in G}
G.add_edge(0, 0)
G.edges[(0, 0)]["label"] = "Loop"
G.edges[(0, 0)]["label_options"] = "midway"
G.nodes[0]["style"] = "blue"
G.nodes[1]["style"] = "line width=3,draw"
G.nodes[2]["style"] = "circle,draw,blue!50"
G.nodes[3]["label"] = "Stop"
G.edges[(0, 1)]["label"] = "1st Step"
G.edges[(0, 1)]["label_options"] = "near end"
G.edges[(2, 3)]["label"] = "3rd Step"
G.edges[(2, 3)]["label_options"] = "near start"
G.edges[(2, 3)]["style"] = "bend left,green"
G.edges[(1, 2)]["label"] = "2nd"
G.edges[(1, 2)]["label_options"] = "pos=0.5"
G.edges[(1, 2)]["style"] = ">->,bend right,line width=3,green!90"
output_tex = nx.to_latex(
G,
pos=pos,
as_document=False,
tikz_options="[scale=3]",
node_options="style",
edge_options="style",
node_label="label",
edge_label="label",
edge_label_options="label_options",
)
expected_tex = r"""\begin{figure}
\begin{tikzpicture}[scale=3]
\draw
(0, 0) node[blue] (0){0}
(1, 1) node[line width=3,draw] (1){1}
(2, 2) node[circle,draw,blue!50] (2){2}
(3, 3) node (3){Stop};
\begin{scope}[->]
\draw (0) to node[near end] {1st Step} (1);
\draw[loop,] (0) to node[midway] {Loop} (0);
\draw[>->,bend right,line width=3,green!90] (1) to node[pos=0.5] {2nd} (2);
\draw[bend left,green] (2) to node[near start] {3rd Step} (3);
\end{scope}
\end{tikzpicture}
\end{figure}"""
assert output_tex == expected_tex
# print(output_tex)
# # Pretty way to assert that A.to_document() == expected_tex
# content_same = True
# for aa, bb in zip(expected_tex.split("\n"), output_tex.split("\n")):
# if aa != bb:
# content_same = False
# print(f"-{aa}|\n+{bb}|")
# assert content_same
def test_basic_multiple_graphs():
H1 = nx.path_graph(4)
H2 = nx.complete_graph(4)
H3 = nx.path_graph(8)
H4 = nx.complete_graph(8)
captions = [
"Path on 4 nodes",
"Complete graph on 4 nodes",
"Path on 8 nodes",
"Complete graph on 8 nodes",
]
labels = ["fig2a", "fig2b", "fig2c", "fig2d"]
latex_code = nx.to_latex(
[H1, H2, H3, H4],
n_rows=2,
sub_captions=captions,
sub_labels=labels,
)
# print(latex_code)
assert "begin{document}" in latex_code
assert "begin{figure}" in latex_code
assert latex_code.count("begin{subfigure}") == 4
assert latex_code.count("tikzpicture") == 8
assert latex_code.count("[-]") == 4
def test_basic_tikz():
expected_tex = r"""\documentclass{report}
\usepackage{tikz}
\usepackage{subcaption}
\begin{document}
\begin{figure}
\begin{subfigure}{0.5\textwidth}
\begin{tikzpicture}[scale=2]
\draw[gray!90]
(0.749, 0.702) node[red!90] (0){0}
(1.0, -0.014) node[red!90] (1){1}
(-0.777, -0.705) node (2){2}
(-0.984, 0.042) node (3){3}
(-0.028, 0.375) node[cyan!90] (4){4}
(-0.412, 0.888) node (5){5}
(0.448, -0.856) node (6){6}
(0.003, -0.431) node[cyan!90] (7){7};
\begin{scope}[->,gray!90]
\draw (0) to (4);
\draw (0) to (5);
\draw (0) to (6);
\draw (0) to (7);
\draw (1) to (4);
\draw (1) to (5);
\draw (1) to (6);
\draw (1) to (7);
\draw (2) to (4);
\draw (2) to (5);
\draw (2) to (6);
\draw (2) to (7);
\draw (3) to (4);
\draw (3) to (5);
\draw (3) to (6);
\draw (3) to (7);
\end{scope}
\end{tikzpicture}
\caption{My tikz number 1 of 2}\label{tikz_1_2}
\end{subfigure}
\begin{subfigure}{0.5\textwidth}
\begin{tikzpicture}[scale=2]
\draw[gray!90]
(0.749, 0.702) node[green!90] (0){0}
(1.0, -0.014) node[green!90] (1){1}
(-0.777, -0.705) node (2){2}
(-0.984, 0.042) node (3){3}
(-0.028, 0.375) node[purple!90] (4){4}
(-0.412, 0.888) node (5){5}
(0.448, -0.856) node (6){6}
(0.003, -0.431) node[purple!90] (7){7};
\begin{scope}[->,gray!90]
\draw (0) to (4);
\draw (0) to (5);
\draw (0) to (6);
\draw (0) to (7);
\draw (1) to (4);
\draw (1) to (5);
\draw (1) to (6);
\draw (1) to (7);
\draw (2) to (4);
\draw (2) to (5);
\draw (2) to (6);
\draw (2) to (7);
\draw (3) to (4);
\draw (3) to (5);
\draw (3) to (6);
\draw (3) to (7);
\end{scope}
\end{tikzpicture}
\caption{My tikz number 2 of 2}\label{tikz_2_2}
\end{subfigure}
\caption{A graph generated with python and latex.}
\end{figure}
\end{document}"""
edges = [
(0, 4),
(0, 5),
(0, 6),
(0, 7),
(1, 4),
(1, 5),
(1, 6),
(1, 7),
(2, 4),
(2, 5),
(2, 6),
(2, 7),
(3, 4),
(3, 5),
(3, 6),
(3, 7),
]
G = nx.DiGraph()
G.add_nodes_from(range(8))
G.add_edges_from(edges)
pos = {
0: (0.7490296171687696, 0.702353520257394),
1: (1.0, -0.014221357723796535),
2: (-0.7765783344161441, -0.7054170966808919),
3: (-0.9842690223417624, 0.04177547602465483),
4: (-0.02768523817180917, 0.3745724439551441),
5: (-0.41154855146767433, 0.8880106515525136),
6: (0.44780153389148264, -0.8561492709269164),
7: (0.0032499953371383505, -0.43092436645809945),
}
rc_node_color = {0: "red!90", 1: "red!90", 4: "cyan!90", 7: "cyan!90"}
gp_node_color = {0: "green!90", 1: "green!90", 4: "purple!90", 7: "purple!90"}
H = G.copy()
nx.set_node_attributes(G, rc_node_color, "color")
nx.set_node_attributes(H, gp_node_color, "color")
sub_captions = ["My tikz number 1 of 2", "My tikz number 2 of 2"]
sub_labels = ["tikz_1_2", "tikz_2_2"]
output_tex = nx.to_latex(
[G, H],
[pos, pos],
tikz_options="[scale=2]",
default_node_options="gray!90",
default_edge_options="gray!90",
node_options="color",
sub_captions=sub_captions,
sub_labels=sub_labels,
caption="A graph generated with python and latex.",
n_rows=2,
as_document=True,
)
assert output_tex == expected_tex
# print(output_tex)
# # Pretty way to assert that A.to_document() == expected_tex
# content_same = True
# for aa, bb in zip(expected_tex.split("\n"), output_tex.split("\n")):
# if aa != bb:
# content_same = False
# print(f"-{aa}|\n+{bb}|")
# assert content_same
def test_exception_pos_single_graph(to_latex=nx.to_latex):
# smoke test that pos can be a string
G = nx.path_graph(4)
to_latex(G, pos="pos")
# must include all nodes
pos = {0: (1, 2), 1: (0, 1), 2: (2, 1)}
with pytest.raises(nx.NetworkXError):
to_latex(G, pos)
# must have 2 values
pos[3] = (1, 2, 3)
with pytest.raises(nx.NetworkXError):
to_latex(G, pos)
pos[3] = 2
with pytest.raises(nx.NetworkXError):
to_latex(G, pos)
# check that passes with 2 values
pos[3] = (3, 2)
to_latex(G, pos)
def test_exception_multiple_graphs(to_latex=nx.to_latex):
G = nx.path_graph(3)
pos_bad = {0: (1, 2), 1: (0, 1)}
pos_OK = {0: (1, 2), 1: (0, 1), 2: (2, 1)}
fourG = [G, G, G, G]
fourpos = [pos_OK, pos_OK, pos_OK, pos_OK]
# input single dict to use for all graphs
to_latex(fourG, pos_OK)
with pytest.raises(nx.NetworkXError):
to_latex(fourG, pos_bad)
# input list of dicts to use for all graphs
to_latex(fourG, fourpos)
with pytest.raises(nx.NetworkXError):
to_latex(fourG, [pos_bad, pos_bad, pos_bad, pos_bad])
# every pos dict must include all nodes
with pytest.raises(nx.NetworkXError):
to_latex(fourG, [pos_OK, pos_OK, pos_bad, pos_OK])
# test sub_captions and sub_labels (len must match Gbunch)
with pytest.raises(nx.NetworkXError):
to_latex(fourG, fourpos, sub_captions=["hi", "hi"])
with pytest.raises(nx.NetworkXError):
to_latex(fourG, fourpos, sub_labels=["hi", "hi"])
# all pass
to_latex(fourG, fourpos, sub_captions=["hi"] * 4, sub_labels=["lbl"] * 4)
def test_exception_multigraph():
G = nx.path_graph(4, create_using=nx.MultiGraph)
G.add_edge(1, 2)
with pytest.raises(nx.NetworkXNotImplemented):
nx.to_latex(G)

View File

@ -0,0 +1,538 @@
"""Unit tests for layout functions."""
import pytest
import networkx as nx
np = pytest.importorskip("numpy")
pytest.importorskip("scipy")
class TestLayout:
@classmethod
def setup_class(cls):
cls.Gi = nx.grid_2d_graph(5, 5)
cls.Gs = nx.Graph()
nx.add_path(cls.Gs, "abcdef")
cls.bigG = nx.grid_2d_graph(25, 25) # > 500 nodes for sparse
def test_spring_fixed_without_pos(self):
G = nx.path_graph(4)
pytest.raises(ValueError, nx.spring_layout, G, fixed=[0])
pos = {0: (1, 1), 2: (0, 0)}
pytest.raises(ValueError, nx.spring_layout, G, fixed=[0, 1], pos=pos)
nx.spring_layout(G, fixed=[0, 2], pos=pos) # No ValueError
def test_spring_init_pos(self):
# Tests GH #2448
import math
G = nx.Graph()
G.add_edges_from([(0, 1), (1, 2), (2, 0), (2, 3)])
init_pos = {0: (0.0, 0.0)}
fixed_pos = [0]
pos = nx.fruchterman_reingold_layout(G, pos=init_pos, fixed=fixed_pos)
has_nan = any(math.isnan(c) for coords in pos.values() for c in coords)
assert not has_nan, "values should not be nan"
def test_smoke_empty_graph(self):
G = []
nx.random_layout(G)
nx.circular_layout(G)
nx.planar_layout(G)
nx.spring_layout(G)
nx.fruchterman_reingold_layout(G)
nx.spectral_layout(G)
nx.shell_layout(G)
nx.bipartite_layout(G, G)
nx.spiral_layout(G)
nx.multipartite_layout(G)
nx.kamada_kawai_layout(G)
def test_smoke_int(self):
G = self.Gi
nx.random_layout(G)
nx.circular_layout(G)
nx.planar_layout(G)
nx.spring_layout(G)
nx.forceatlas2_layout(G)
nx.fruchterman_reingold_layout(G)
nx.fruchterman_reingold_layout(self.bigG)
nx.spectral_layout(G)
nx.spectral_layout(G.to_directed())
nx.spectral_layout(self.bigG)
nx.spectral_layout(self.bigG.to_directed())
nx.shell_layout(G)
nx.spiral_layout(G)
nx.kamada_kawai_layout(G)
nx.kamada_kawai_layout(G, dim=1)
nx.kamada_kawai_layout(G, dim=3)
nx.arf_layout(G)
def test_smoke_string(self):
G = self.Gs
nx.random_layout(G)
nx.circular_layout(G)
nx.planar_layout(G)
nx.spring_layout(G)
nx.forceatlas2_layout(G)
nx.fruchterman_reingold_layout(G)
nx.spectral_layout(G)
nx.shell_layout(G)
nx.spiral_layout(G)
nx.kamada_kawai_layout(G)
nx.kamada_kawai_layout(G, dim=1)
nx.kamada_kawai_layout(G, dim=3)
nx.arf_layout(G)
def check_scale_and_center(self, pos, scale, center):
center = np.array(center)
low = center - scale
hi = center + scale
vpos = np.array(list(pos.values()))
length = vpos.max(0) - vpos.min(0)
assert (length <= 2 * scale).all()
assert (vpos >= low).all()
assert (vpos <= hi).all()
def test_scale_and_center_arg(self):
sc = self.check_scale_and_center
c = (4, 5)
G = nx.complete_graph(9)
G.add_node(9)
sc(nx.random_layout(G, center=c), scale=0.5, center=(4.5, 5.5))
# rest can have 2*scale length: [-scale, scale]
sc(nx.spring_layout(G, scale=2, center=c), scale=2, center=c)
sc(nx.spectral_layout(G, scale=2, center=c), scale=2, center=c)
sc(nx.circular_layout(G, scale=2, center=c), scale=2, center=c)
sc(nx.shell_layout(G, scale=2, center=c), scale=2, center=c)
sc(nx.spiral_layout(G, scale=2, center=c), scale=2, center=c)
sc(nx.kamada_kawai_layout(G, scale=2, center=c), scale=2, center=c)
c = (2, 3, 5)
sc(nx.kamada_kawai_layout(G, dim=3, scale=2, center=c), scale=2, center=c)
def test_planar_layout_non_planar_input(self):
G = nx.complete_graph(9)
pytest.raises(nx.NetworkXException, nx.planar_layout, G)
def test_smoke_planar_layout_embedding_input(self):
embedding = nx.PlanarEmbedding()
embedding.set_data({0: [1, 2], 1: [0, 2], 2: [0, 1]})
nx.planar_layout(embedding)
def test_default_scale_and_center(self):
sc = self.check_scale_and_center
c = (0, 0)
G = nx.complete_graph(9)
G.add_node(9)
sc(nx.random_layout(G), scale=0.5, center=(0.5, 0.5))
sc(nx.spring_layout(G), scale=1, center=c)
sc(nx.spectral_layout(G), scale=1, center=c)
sc(nx.circular_layout(G), scale=1, center=c)
sc(nx.shell_layout(G), scale=1, center=c)
sc(nx.spiral_layout(G), scale=1, center=c)
sc(nx.kamada_kawai_layout(G), scale=1, center=c)
c = (0, 0, 0)
sc(nx.kamada_kawai_layout(G, dim=3), scale=1, center=c)
def test_circular_planar_and_shell_dim_error(self):
G = nx.path_graph(4)
pytest.raises(ValueError, nx.circular_layout, G, dim=1)
pytest.raises(ValueError, nx.shell_layout, G, dim=1)
pytest.raises(ValueError, nx.shell_layout, G, dim=3)
pytest.raises(ValueError, nx.planar_layout, G, dim=1)
pytest.raises(ValueError, nx.planar_layout, G, dim=3)
def test_adjacency_interface_numpy(self):
A = nx.to_numpy_array(self.Gs)
pos = nx.drawing.layout._fruchterman_reingold(A)
assert pos.shape == (6, 2)
pos = nx.drawing.layout._fruchterman_reingold(A, dim=3)
assert pos.shape == (6, 3)
pos = nx.drawing.layout._sparse_fruchterman_reingold(A)
assert pos.shape == (6, 2)
def test_adjacency_interface_scipy(self):
A = nx.to_scipy_sparse_array(self.Gs, dtype="d")
pos = nx.drawing.layout._sparse_fruchterman_reingold(A)
assert pos.shape == (6, 2)
pos = nx.drawing.layout._sparse_spectral(A)
assert pos.shape == (6, 2)
pos = nx.drawing.layout._sparse_fruchterman_reingold(A, dim=3)
assert pos.shape == (6, 3)
def test_single_nodes(self):
G = nx.path_graph(1)
vpos = nx.shell_layout(G)
assert not vpos[0].any()
G = nx.path_graph(4)
vpos = nx.shell_layout(G, [[0], [1, 2], [3]])
assert not vpos[0].any()
assert vpos[3].any() # ensure node 3 not at origin (#3188)
assert np.linalg.norm(vpos[3]) <= 1 # ensure node 3 fits (#3753)
vpos = nx.shell_layout(G, [[0], [1, 2], [3]], rotate=0)
assert np.linalg.norm(vpos[3]) <= 1 # ensure node 3 fits (#3753)
def test_smoke_initial_pos_forceatlas2(self):
pos = nx.circular_layout(self.Gi)
npos = nx.forceatlas2_layout(self.Gi, pos=pos)
def test_smoke_initial_pos_fruchterman_reingold(self):
pos = nx.circular_layout(self.Gi)
npos = nx.fruchterman_reingold_layout(self.Gi, pos=pos)
def test_smoke_initial_pos_arf(self):
pos = nx.circular_layout(self.Gi)
npos = nx.arf_layout(self.Gi, pos=pos)
def test_fixed_node_fruchterman_reingold(self):
# Dense version (numpy based)
pos = nx.circular_layout(self.Gi)
npos = nx.spring_layout(self.Gi, pos=pos, fixed=[(0, 0)])
assert tuple(pos[(0, 0)]) == tuple(npos[(0, 0)])
# Sparse version (scipy based)
pos = nx.circular_layout(self.bigG)
npos = nx.spring_layout(self.bigG, pos=pos, fixed=[(0, 0)])
for axis in range(2):
assert pos[(0, 0)][axis] == pytest.approx(npos[(0, 0)][axis], abs=1e-7)
def test_center_parameter(self):
G = nx.path_graph(1)
nx.random_layout(G, center=(1, 1))
vpos = nx.circular_layout(G, center=(1, 1))
assert tuple(vpos[0]) == (1, 1)
vpos = nx.planar_layout(G, center=(1, 1))
assert tuple(vpos[0]) == (1, 1)
vpos = nx.spring_layout(G, center=(1, 1))
assert tuple(vpos[0]) == (1, 1)
vpos = nx.fruchterman_reingold_layout(G, center=(1, 1))
assert tuple(vpos[0]) == (1, 1)
vpos = nx.spectral_layout(G, center=(1, 1))
assert tuple(vpos[0]) == (1, 1)
vpos = nx.shell_layout(G, center=(1, 1))
assert tuple(vpos[0]) == (1, 1)
vpos = nx.spiral_layout(G, center=(1, 1))
assert tuple(vpos[0]) == (1, 1)
def test_center_wrong_dimensions(self):
G = nx.path_graph(1)
assert id(nx.spring_layout) == id(nx.fruchterman_reingold_layout)
pytest.raises(ValueError, nx.random_layout, G, center=(1, 1, 1))
pytest.raises(ValueError, nx.circular_layout, G, center=(1, 1, 1))
pytest.raises(ValueError, nx.planar_layout, G, center=(1, 1, 1))
pytest.raises(ValueError, nx.spring_layout, G, center=(1, 1, 1))
pytest.raises(ValueError, nx.spring_layout, G, dim=3, center=(1, 1))
pytest.raises(ValueError, nx.spectral_layout, G, center=(1, 1, 1))
pytest.raises(ValueError, nx.spectral_layout, G, dim=3, center=(1, 1))
pytest.raises(ValueError, nx.shell_layout, G, center=(1, 1, 1))
pytest.raises(ValueError, nx.spiral_layout, G, center=(1, 1, 1))
pytest.raises(ValueError, nx.kamada_kawai_layout, G, center=(1, 1, 1))
def test_empty_graph(self):
G = nx.empty_graph()
vpos = nx.random_layout(G, center=(1, 1))
assert vpos == {}
vpos = nx.circular_layout(G, center=(1, 1))
assert vpos == {}
vpos = nx.planar_layout(G, center=(1, 1))
assert vpos == {}
vpos = nx.bipartite_layout(G, G)
assert vpos == {}
vpos = nx.spring_layout(G, center=(1, 1))
assert vpos == {}
vpos = nx.fruchterman_reingold_layout(G, center=(1, 1))
assert vpos == {}
vpos = nx.spectral_layout(G, center=(1, 1))
assert vpos == {}
vpos = nx.shell_layout(G, center=(1, 1))
assert vpos == {}
vpos = nx.spiral_layout(G, center=(1, 1))
assert vpos == {}
vpos = nx.multipartite_layout(G, center=(1, 1))
assert vpos == {}
vpos = nx.kamada_kawai_layout(G, center=(1, 1))
assert vpos == {}
vpos = nx.forceatlas2_layout(G)
assert vpos == {}
vpos = nx.arf_layout(G)
assert vpos == {}
def test_bipartite_layout(self):
G = nx.complete_bipartite_graph(3, 5)
top, bottom = nx.bipartite.sets(G)
vpos = nx.bipartite_layout(G, top)
assert len(vpos) == len(G)
top_x = vpos[list(top)[0]][0]
bottom_x = vpos[list(bottom)[0]][0]
for node in top:
assert vpos[node][0] == top_x
for node in bottom:
assert vpos[node][0] == bottom_x
vpos = nx.bipartite_layout(
G, top, align="horizontal", center=(2, 2), scale=2, aspect_ratio=1
)
assert len(vpos) == len(G)
top_y = vpos[list(top)[0]][1]
bottom_y = vpos[list(bottom)[0]][1]
for node in top:
assert vpos[node][1] == top_y
for node in bottom:
assert vpos[node][1] == bottom_y
pytest.raises(ValueError, nx.bipartite_layout, G, top, align="foo")
def test_multipartite_layout(self):
sizes = (0, 5, 7, 2, 8)
G = nx.complete_multipartite_graph(*sizes)
vpos = nx.multipartite_layout(G)
assert len(vpos) == len(G)
start = 0
for n in sizes:
end = start + n
assert all(vpos[start][0] == vpos[i][0] for i in range(start + 1, end))
start += n
vpos = nx.multipartite_layout(G, align="horizontal", scale=2, center=(2, 2))
assert len(vpos) == len(G)
start = 0
for n in sizes:
end = start + n
assert all(vpos[start][1] == vpos[i][1] for i in range(start + 1, end))
start += n
pytest.raises(ValueError, nx.multipartite_layout, G, align="foo")
def test_kamada_kawai_costfn_1d(self):
costfn = nx.drawing.layout._kamada_kawai_costfn
pos = np.array([4.0, 7.0])
invdist = 1 / np.array([[0.1, 2.0], [2.0, 0.3]])
cost, grad = costfn(pos, np, invdist, meanweight=0, dim=1)
assert cost == pytest.approx(((3 / 2.0 - 1) ** 2), abs=1e-7)
assert grad[0] == pytest.approx((-0.5), abs=1e-7)
assert grad[1] == pytest.approx(0.5, abs=1e-7)
def check_kamada_kawai_costfn(self, pos, invdist, meanwt, dim):
costfn = nx.drawing.layout._kamada_kawai_costfn
cost, grad = costfn(pos.ravel(), np, invdist, meanweight=meanwt, dim=dim)
expected_cost = 0.5 * meanwt * np.sum(np.sum(pos, axis=0) ** 2)
for i in range(pos.shape[0]):
for j in range(i + 1, pos.shape[0]):
diff = np.linalg.norm(pos[i] - pos[j])
expected_cost += (diff * invdist[i][j] - 1.0) ** 2
assert cost == pytest.approx(expected_cost, abs=1e-7)
dx = 1e-4
for nd in range(pos.shape[0]):
for dm in range(pos.shape[1]):
idx = nd * pos.shape[1] + dm
ps = pos.flatten()
ps[idx] += dx
cplus = costfn(ps, np, invdist, meanweight=meanwt, dim=pos.shape[1])[0]
ps[idx] -= 2 * dx
cminus = costfn(ps, np, invdist, meanweight=meanwt, dim=pos.shape[1])[0]
assert grad[idx] == pytest.approx((cplus - cminus) / (2 * dx), abs=1e-5)
def test_kamada_kawai_costfn(self):
invdist = 1 / np.array([[0.1, 2.1, 1.7], [2.1, 0.2, 0.6], [1.7, 0.6, 0.3]])
meanwt = 0.3
# 2d
pos = np.array([[1.3, -3.2], [2.7, -0.3], [5.1, 2.5]])
self.check_kamada_kawai_costfn(pos, invdist, meanwt, 2)
# 3d
pos = np.array([[0.9, 8.6, -8.7], [-10, -0.5, -7.1], [9.1, -8.1, 1.6]])
self.check_kamada_kawai_costfn(pos, invdist, meanwt, 3)
def test_spiral_layout(self):
G = self.Gs
# a lower value of resolution should result in a more compact layout
# intuitively, the total distance from the start and end nodes
# via each node in between (transiting through each) will be less,
# assuming rescaling does not occur on the computed node positions
pos_standard = np.array(list(nx.spiral_layout(G, resolution=0.35).values()))
pos_tighter = np.array(list(nx.spiral_layout(G, resolution=0.34).values()))
distances = np.linalg.norm(pos_standard[:-1] - pos_standard[1:], axis=1)
distances_tighter = np.linalg.norm(pos_tighter[:-1] - pos_tighter[1:], axis=1)
assert sum(distances) > sum(distances_tighter)
# return near-equidistant points after the first value if set to true
pos_equidistant = np.array(list(nx.spiral_layout(G, equidistant=True).values()))
distances_equidistant = np.linalg.norm(
pos_equidistant[:-1] - pos_equidistant[1:], axis=1
)
assert np.allclose(
distances_equidistant[1:], distances_equidistant[-1], atol=0.01
)
def test_spiral_layout_equidistant(self):
G = nx.path_graph(10)
pos = nx.spiral_layout(G, equidistant=True)
# Extract individual node positions as an array
p = np.array(list(pos.values()))
# Elementwise-distance between node positions
dist = np.linalg.norm(p[1:] - p[:-1], axis=1)
assert np.allclose(np.diff(dist), 0, atol=1e-3)
def test_forceatlas2_layout_partial_input_test(self):
# check whether partial pos input still returns a full proper position
G = self.Gs
node = nx.utils.arbitrary_element(G)
pos = nx.circular_layout(G)
del pos[node]
pos = nx.forceatlas2_layout(G, pos=pos)
assert len(pos) == len(G)
def test_rescale_layout_dict(self):
G = nx.empty_graph()
vpos = nx.random_layout(G, center=(1, 1))
assert nx.rescale_layout_dict(vpos) == {}
G = nx.empty_graph(2)
vpos = {0: (0.0, 0.0), 1: (1.0, 1.0)}
s_vpos = nx.rescale_layout_dict(vpos)
assert np.linalg.norm([sum(x) for x in zip(*s_vpos.values())]) < 1e-6
G = nx.empty_graph(3)
vpos = {0: (0, 0), 1: (1, 1), 2: (0.5, 0.5)}
s_vpos = nx.rescale_layout_dict(vpos)
expectation = {
0: np.array((-1, -1)),
1: np.array((1, 1)),
2: np.array((0, 0)),
}
for k, v in expectation.items():
assert (s_vpos[k] == v).all()
s_vpos = nx.rescale_layout_dict(vpos, scale=2)
expectation = {
0: np.array((-2, -2)),
1: np.array((2, 2)),
2: np.array((0, 0)),
}
for k, v in expectation.items():
assert (s_vpos[k] == v).all()
def test_arf_layout_partial_input_test(self):
# Checks whether partial pos input still returns a proper position.
G = self.Gs
node = nx.utils.arbitrary_element(G)
pos = nx.circular_layout(G)
del pos[node]
pos = nx.arf_layout(G, pos=pos)
assert len(pos) == len(G)
def test_arf_layout_negative_a_check(self):
"""
Checks input parameters correctly raises errors. For example, `a` should be larger than 1
"""
G = self.Gs
pytest.raises(ValueError, nx.arf_layout, G=G, a=-1)
def test_smoke_seed_input(self):
G = self.Gs
nx.random_layout(G, seed=42)
nx.spring_layout(G, seed=42)
nx.arf_layout(G, seed=42)
nx.forceatlas2_layout(G, seed=42)
def test_multipartite_layout_nonnumeric_partition_labels():
"""See gh-5123."""
G = nx.Graph()
G.add_node(0, subset="s0")
G.add_node(1, subset="s0")
G.add_node(2, subset="s1")
G.add_node(3, subset="s1")
G.add_edges_from([(0, 2), (0, 3), (1, 2)])
pos = nx.multipartite_layout(G)
assert len(pos) == len(G)
def test_multipartite_layout_layer_order():
"""Return the layers in sorted order if the layers of the multipartite
graph are sortable. See gh-5691"""
G = nx.Graph()
node_group = dict(zip(("a", "b", "c", "d", "e"), (2, 3, 1, 2, 4)))
for node, layer in node_group.items():
G.add_node(node, subset=layer)
# Horizontal alignment, therefore y-coord determines layers
pos = nx.multipartite_layout(G, align="horizontal")
layers = nx.utils.groups(node_group)
pos_from_layers = nx.multipartite_layout(G, align="horizontal", subset_key=layers)
for (n1, p1), (n2, p2) in zip(pos.items(), pos_from_layers.items()):
assert n1 == n2 and (p1 == p2).all()
# Nodes "a" and "d" are in the same layer
assert pos["a"][-1] == pos["d"][-1]
# positions should be sorted according to layer
assert pos["c"][-1] < pos["a"][-1] < pos["b"][-1] < pos["e"][-1]
# Make sure that multipartite_layout still works when layers are not sortable
G.nodes["a"]["subset"] = "layer_0" # Can't sort mixed strs/ints
pos_nosort = nx.multipartite_layout(G) # smoke test: this should not raise
assert pos_nosort.keys() == pos.keys()
def _num_nodes_per_bfs_layer(pos):
"""Helper function to extract the number of nodes in each layer of bfs_layout"""
x = np.array(list(pos.values()))[:, 0] # node positions in layered dimension
_, layer_count = np.unique(x, return_counts=True)
return layer_count
@pytest.mark.parametrize("n", range(2, 7))
def test_bfs_layout_complete_graph(n):
"""The complete graph should result in two layers: the starting node and
a second layer containing all neighbors."""
G = nx.complete_graph(n)
pos = nx.bfs_layout(G, start=0)
assert np.array_equal(_num_nodes_per_bfs_layer(pos), [1, n - 1])
def test_bfs_layout_barbell():
G = nx.barbell_graph(5, 3)
# Start in one of the "bells"
pos = nx.bfs_layout(G, start=0)
# start, bell-1, [1] * len(bar)+1, bell-1
expected_nodes_per_layer = [1, 4, 1, 1, 1, 1, 4]
assert np.array_equal(_num_nodes_per_bfs_layer(pos), expected_nodes_per_layer)
# Start in the other "bell" - expect same layer pattern
pos = nx.bfs_layout(G, start=12)
assert np.array_equal(_num_nodes_per_bfs_layer(pos), expected_nodes_per_layer)
# Starting in the center of the bar, expect layers to be symmetric
pos = nx.bfs_layout(G, start=6)
# Expected layers: {6 (start)}, {5, 7}, {4, 8}, {8 nodes from remainder of bells}
expected_nodes_per_layer = [1, 2, 2, 8]
assert np.array_equal(_num_nodes_per_bfs_layer(pos), expected_nodes_per_layer)
def test_bfs_layout_disconnected():
G = nx.complete_graph(5)
G.add_edges_from([(10, 11), (11, 12)])
with pytest.raises(nx.NetworkXError, match="bfs_layout didn't include all nodes"):
nx.bfs_layout(G, start=0)

View File

@ -0,0 +1,146 @@
"""Unit tests for pydot drawing functions."""
from io import StringIO
import pytest
import networkx as nx
from networkx.utils import graphs_equal
pydot = pytest.importorskip("pydot")
class TestPydot:
@pytest.mark.parametrize("G", (nx.Graph(), nx.DiGraph()))
@pytest.mark.parametrize("prog", ("neato", "dot"))
def test_pydot(self, G, prog, tmp_path):
"""
Validate :mod:`pydot`-based usage of the passed NetworkX graph with the
passed basename of an external GraphViz command (e.g., `dot`, `neato`).
"""
# Set the name of this graph to... "G". Failing to do so will
# subsequently trip an assertion expecting this name.
G.graph["name"] = "G"
# Add arbitrary nodes and edges to the passed empty graph.
G.add_edges_from([("A", "B"), ("A", "C"), ("B", "C"), ("A", "D")])
G.add_node("E")
# Validate layout of this graph with the passed GraphViz command.
graph_layout = nx.nx_pydot.pydot_layout(G, prog=prog)
assert isinstance(graph_layout, dict)
# Convert this graph into a "pydot.Dot" instance.
P = nx.nx_pydot.to_pydot(G)
# Convert this "pydot.Dot" instance back into a graph of the same type.
G2 = G.__class__(nx.nx_pydot.from_pydot(P))
# Validate the original and resulting graphs to be the same.
assert graphs_equal(G, G2)
fname = tmp_path / "out.dot"
# Serialize this "pydot.Dot" instance to a temporary file in dot format
P.write_raw(fname)
# Deserialize a list of new "pydot.Dot" instances back from this file.
Pin_list = pydot.graph_from_dot_file(path=fname, encoding="utf-8")
# Validate this file to contain only one graph.
assert len(Pin_list) == 1
# The single "pydot.Dot" instance deserialized from this file.
Pin = Pin_list[0]
# Sorted list of all nodes in the original "pydot.Dot" instance.
n1 = sorted(p.get_name() for p in P.get_node_list())
# Sorted list of all nodes in the deserialized "pydot.Dot" instance.
n2 = sorted(p.get_name() for p in Pin.get_node_list())
# Validate these instances to contain the same nodes.
assert n1 == n2
# Sorted list of all edges in the original "pydot.Dot" instance.
e1 = sorted((e.get_source(), e.get_destination()) for e in P.get_edge_list())
# Sorted list of all edges in the original "pydot.Dot" instance.
e2 = sorted((e.get_source(), e.get_destination()) for e in Pin.get_edge_list())
# Validate these instances to contain the same edges.
assert e1 == e2
# Deserialize a new graph of the same type back from this file.
Hin = nx.nx_pydot.read_dot(fname)
Hin = G.__class__(Hin)
# Validate the original and resulting graphs to be the same.
assert graphs_equal(G, Hin)
def test_read_write(self):
G = nx.MultiGraph()
G.graph["name"] = "G"
G.add_edge("1", "2", key="0") # read assumes strings
fh = StringIO()
nx.nx_pydot.write_dot(G, fh)
fh.seek(0)
H = nx.nx_pydot.read_dot(fh)
assert graphs_equal(G, H)
def test_pydot_issue_7581(tmp_path):
"""Validate that `nx_pydot.pydot_layout` handles nodes
with characters like "\n", " ".
Those characters cause `pydot` to escape and quote them on output,
which caused #7581.
"""
G = nx.Graph()
G.add_edges_from([("A\nbig test", "B"), ("A\nbig test", "C"), ("B", "C")])
graph_layout = nx.nx_pydot.pydot_layout(G, prog="dot")
assert isinstance(graph_layout, dict)
# Convert the graph to pydot and back into a graph. There should be no difference.
P = nx.nx_pydot.to_pydot(G)
G2 = nx.Graph(nx.nx_pydot.from_pydot(P))
assert graphs_equal(G, G2)
@pytest.mark.parametrize(
"graph_type", [nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph]
)
def test_hashable_pydot(graph_type):
# gh-5790
G = graph_type()
G.add_edge("5", frozenset([1]), t='"Example:A"', l=False)
G.add_edge("1", 2, w=True, t=("node1",), l=frozenset(["node1"]))
G.add_edge("node", (3, 3), w="string")
assert [
{"t": '"Example:A"', "l": "False"},
{"w": "True", "t": "('node1',)", "l": "frozenset({'node1'})"},
{"w": "string"},
] == [
attr
for _, _, attr in nx.nx_pydot.from_pydot(nx.nx_pydot.to_pydot(G)).edges.data()
]
assert {str(i) for i in G.nodes()} == set(
nx.nx_pydot.from_pydot(nx.nx_pydot.to_pydot(G)).nodes
)
def test_pydot_numerical_name():
G = nx.Graph()
G.add_edges_from([("A", "B"), (0, 1)])
graph_layout = nx.nx_pydot.pydot_layout(G, prog="dot")
assert isinstance(graph_layout, dict)
assert "0" not in graph_layout
assert 0 in graph_layout
assert "1" not in graph_layout
assert 1 in graph_layout
assert "A" in graph_layout
assert "B" in graph_layout

File diff suppressed because it is too large Load Diff