Restore 0.1.5 version from stash
This commit is contained in:
BIN
Binary file not shown.
|
After Width: | Height: | Size: 139 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
@@ -0,0 +1,237 @@
|
||||
"""Unit tests for PyGraphviz interface."""
|
||||
|
||||
import warnings
|
||||
|
||||
import pytest
|
||||
|
||||
import networkx as nx
|
||||
from networkx.utils import edges_equal, graphs_equal, nodes_equal
|
||||
|
||||
pygraphviz = pytest.importorskip("pygraphviz")
|
||||
|
||||
|
||||
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(), directed=G1.is_directed())
|
||||
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)
|
||||
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
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
"""Unit tests for explicit image comparison with pytest-mpl."""
|
||||
|
||||
import pytest
|
||||
|
||||
import networkx as nx
|
||||
|
||||
pytest.importorskip("pytest_mpl")
|
||||
|
||||
mpl = pytest.importorskip("matplotlib")
|
||||
mpl.use("PS")
|
||||
plt = pytest.importorskip("matplotlib.pyplot")
|
||||
plt.rcParams["text.usetex"] = False
|
||||
np = pytest.importorskip("numpy")
|
||||
|
||||
|
||||
@pytest.mark.mpl_image_compare
|
||||
def test_display_house_with_colors():
|
||||
"""
|
||||
Originally, I wanted to use the exact samge image as test_house_with_colors.
|
||||
But I can't seem to find the correct value for the margins to get the figures
|
||||
to line up perfectly. To the human eye, these visualizations are basically the
|
||||
same.
|
||||
"""
|
||||
G = nx.house_graph()
|
||||
fig, ax = plt.subplots()
|
||||
nx.set_node_attributes(
|
||||
G, {0: (0, 0), 1: (1, 0), 2: (0, 1), 3: (1, 1), 4: (0.5, 2.0)}, "pos"
|
||||
)
|
||||
nx.set_node_attributes(
|
||||
G,
|
||||
{
|
||||
n: {
|
||||
"size": 3000 if n != 4 else 2000,
|
||||
"color": "tab:blue" if n != 4 else "tab:orange",
|
||||
}
|
||||
for n in G.nodes()
|
||||
},
|
||||
)
|
||||
nx.display(
|
||||
G,
|
||||
node_pos="pos",
|
||||
edge_alpha=0.5,
|
||||
edge_width=6,
|
||||
node_label=None,
|
||||
node_border_color="k",
|
||||
)
|
||||
ax.margins(0.17)
|
||||
plt.tight_layout()
|
||||
plt.axis("off")
|
||||
return fig
|
||||
|
||||
|
||||
@pytest.mark.mpl_image_compare
|
||||
def test_display_labels_and_colors():
|
||||
"""See 'Labels and Colors' gallery example"""
|
||||
fig, ax = plt.subplots()
|
||||
G = nx.cubical_graph()
|
||||
pos = nx.spring_layout(G, seed=3113794652) # positions for all nodes
|
||||
nx.set_node_attributes(G, pos, "pos") # Will not be needed after PR 7571
|
||||
labels = iter(
|
||||
[
|
||||
r"$a$",
|
||||
r"$b$",
|
||||
r"$c$",
|
||||
r"$d$",
|
||||
r"$\alpha$",
|
||||
r"$\beta$",
|
||||
r"$\gamma$",
|
||||
r"$\delta$",
|
||||
]
|
||||
)
|
||||
nx.set_node_attributes(
|
||||
G,
|
||||
{
|
||||
n: {
|
||||
"size": 800,
|
||||
"alpha": 0.9,
|
||||
"color": "tab:red" if n < 4 else "tab:blue",
|
||||
"label": {"label": next(labels), "size": 22, "color": "whitesmoke"},
|
||||
}
|
||||
for n in G.nodes()
|
||||
},
|
||||
)
|
||||
|
||||
nx.display(G, node_pos="pos", edge_color="tab:grey")
|
||||
|
||||
# The tricky bit is the highlighted colors for the edges
|
||||
edgelist = [(0, 1), (1, 2), (2, 3), (0, 3)]
|
||||
nx.set_edge_attributes(
|
||||
G,
|
||||
{
|
||||
(u, v): {
|
||||
"width": 8,
|
||||
"alpha": 0.5,
|
||||
"color": "tab:red",
|
||||
"visible": (u, v) in edgelist,
|
||||
}
|
||||
for u, v in G.edges()
|
||||
},
|
||||
)
|
||||
nx.display(G, node_pos="pos", node_visible=False)
|
||||
edgelist = [(4, 5), (5, 6), (6, 7), (4, 7)]
|
||||
nx.set_edge_attributes(
|
||||
G,
|
||||
{
|
||||
(u, v): {
|
||||
"color": "tab:blue",
|
||||
"visible": (u, v) in edgelist,
|
||||
}
|
||||
for u, v in G.edges()
|
||||
},
|
||||
)
|
||||
nx.display(G, node_pos="pos", node_visible=False)
|
||||
|
||||
plt.tight_layout()
|
||||
plt.axis("off")
|
||||
return fig
|
||||
|
||||
|
||||
@pytest.mark.mpl_image_compare
|
||||
def test_display_complex():
|
||||
import itertools as it
|
||||
|
||||
fig, ax = plt.subplots()
|
||||
G = nx.MultiDiGraph()
|
||||
nodes = "ABC"
|
||||
prod = list(it.product(nodes, repeat=2)) * 4
|
||||
G = nx.MultiDiGraph()
|
||||
for i, (u, v) in enumerate(prod):
|
||||
G.add_edge(u, v, w=round(i / 3, 2))
|
||||
nx.set_node_attributes(G, nx.spring_layout(G, seed=3113794652), "pos")
|
||||
csi = it.cycle([f"arc3,rad={r}" for r in it.accumulate([0.15] * 4)])
|
||||
nx.set_edge_attributes(G, {e: next(csi) for e in G.edges(keys=True)}, "curvature")
|
||||
nx.set_edge_attributes(
|
||||
G,
|
||||
{
|
||||
tuple(e): {"label": w, "bbox": {"alpha": 0}}
|
||||
for *e, w in G.edges(keys=True, data="w")
|
||||
},
|
||||
"label",
|
||||
)
|
||||
nx.apply_matplotlib_colors(G, "w", "color", mpl.colormaps["inferno"], nodes=False)
|
||||
nx.display(G, canvas=ax, node_pos="pos")
|
||||
|
||||
plt.tight_layout()
|
||||
plt.axis("off")
|
||||
return fig
|
||||
|
||||
|
||||
@pytest.mark.mpl_image_compare
|
||||
def test_display_shortest_path():
|
||||
fig, ax = plt.subplots()
|
||||
G = nx.Graph()
|
||||
G.add_nodes_from(["A", "B", "C", "D", "E", "F", "G", "H"])
|
||||
G.add_edge("A", "B", weight=4)
|
||||
G.add_edge("A", "H", weight=8)
|
||||
G.add_edge("B", "C", weight=8)
|
||||
G.add_edge("B", "H", weight=11)
|
||||
G.add_edge("C", "D", weight=7)
|
||||
G.add_edge("C", "F", weight=4)
|
||||
G.add_edge("C", "I", weight=2)
|
||||
G.add_edge("D", "E", weight=9)
|
||||
G.add_edge("D", "F", weight=14)
|
||||
G.add_edge("E", "F", weight=10)
|
||||
G.add_edge("F", "G", weight=2)
|
||||
G.add_edge("G", "H", weight=1)
|
||||
G.add_edge("G", "I", weight=6)
|
||||
G.add_edge("H", "I", weight=7)
|
||||
|
||||
# Find the shortest path from node A to node E
|
||||
path = nx.shortest_path(G, "A", "E", weight="weight")
|
||||
|
||||
# Create a list of edges in the shortest path
|
||||
path_edges = list(zip(path, path[1:]))
|
||||
nx.set_node_attributes(G, nx.spring_layout(G, seed=37), "pos")
|
||||
nx.set_edge_attributes(
|
||||
G,
|
||||
{
|
||||
(u, v): {
|
||||
"color": (
|
||||
"red"
|
||||
if (u, v) in path_edges or tuple(reversed((u, v))) in path_edges
|
||||
else "black"
|
||||
),
|
||||
"label": {"label": d["weight"], "rotate": False},
|
||||
}
|
||||
for u, v, d in G.edges(data=True)
|
||||
},
|
||||
)
|
||||
nx.display(G, canvas=ax)
|
||||
plt.tight_layout()
|
||||
plt.axis("off")
|
||||
return fig
|
||||
|
||||
|
||||
@pytest.mark.mpl_image_compare
|
||||
def test_display_empty_graph():
|
||||
G = nx.empty_graph()
|
||||
fig, ax = plt.subplots()
|
||||
nx.display(G, canvas=ax)
|
||||
plt.tight_layout()
|
||||
plt.axis("off")
|
||||
return fig
|
||||
|
||||
|
||||
@pytest.mark.mpl_image_compare
|
||||
def test_house_with_colors():
|
||||
G = nx.house_graph()
|
||||
# explicitly set positions
|
||||
fig, ax = plt.subplots()
|
||||
pos = {0: (0, 0), 1: (1, 0), 2: (0, 1), 3: (1, 1), 4: (0.5, 2.0)}
|
||||
|
||||
# Plot nodes with different properties for the "wall" and "roof" nodes
|
||||
nx.draw_networkx_nodes(
|
||||
G,
|
||||
pos,
|
||||
node_size=3000,
|
||||
nodelist=[0, 1, 2, 3],
|
||||
node_color="tab:blue",
|
||||
)
|
||||
nx.draw_networkx_nodes(
|
||||
G, pos, node_size=2000, nodelist=[4], node_color="tab:orange"
|
||||
)
|
||||
nx.draw_networkx_edges(G, pos, alpha=0.5, width=6)
|
||||
# Customize axes
|
||||
ax.margins(0.11)
|
||||
plt.tight_layout()
|
||||
plt.axis("off")
|
||||
return fig
|
||||
@@ -0,0 +1,285 @@
|
||||
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}"""
|
||||
|
||||
# First, check for consistency line-by-line - if this fails, the mismatched
|
||||
# line will be shown explicitly in the failure summary
|
||||
for expected, actual in zip(expected_tex.split("\n"), output_tex.split("\n")):
|
||||
assert expected == actual
|
||||
|
||||
assert output_tex == expected_tex
|
||||
|
||||
|
||||
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,
|
||||
)
|
||||
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,
|
||||
)
|
||||
|
||||
# First, check for consistency line-by-line - if this fails, the mismatched
|
||||
# line will be shown explicitly in the failure summary
|
||||
for expected, actual in zip(expected_tex.split("\n"), output_tex.split("\n")):
|
||||
assert expected == actual
|
||||
# Double-check for document-level consistency
|
||||
assert output_tex == expected_tex
|
||||
|
||||
|
||||
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)
|
||||
@@ -0,0 +1,631 @@
|
||||
"""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)
|
||||
# No pos dict at all
|
||||
with pytest.raises(ValueError, match="nodes are fixed without positions"):
|
||||
nx.spring_layout(G, fixed=[0])
|
||||
|
||||
pos = {0: (1, 1), 2: (0, 0)}
|
||||
# Node 1 not in pos dict
|
||||
with pytest.raises(ValueError, match="nodes are fixed without positions"):
|
||||
nx.spring_layout(G, fixed=[0, 1], pos=pos)
|
||||
|
||||
# All fixed nodes in pos dict
|
||||
out = nx.spring_layout(G, fixed=[0, 2], pos=pos) # No ValueError
|
||||
assert all(np.array_equal(out[n], pos[n]) for n in (0, 2))
|
||||
|
||||
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)
|
||||
nx.spiral_layout(G, equidistant=True, store_pos_as="pos")
|
||||
pos = nx.get_node_attributes(G, "pos")
|
||||
# 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_node_at_center(self):
|
||||
# see gh-7791 avoid divide by zero
|
||||
G = nx.path_graph(3)
|
||||
orig_pos = {i: [i - 1, 0.0] for i in range(3)}
|
||||
new_pos = nx.forceatlas2_layout(G, pos=orig_pos)
|
||||
|
||||
def test_initial_only_some_pos(self):
|
||||
G = nx.path_graph(3)
|
||||
orig_pos = {i: [i - 1, 0.0] for i in range(2)}
|
||||
new_pos = nx.forceatlas2_layout(G, pos=orig_pos, 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)
|
||||
nx.bfs_layout(G, start=0, store_pos_as="pos")
|
||||
pos = nx.get_node_attributes(G, "pos")
|
||||
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)
|
||||
|
||||
|
||||
def test_bipartite_layout_default_nodes_raises_non_bipartite_input():
|
||||
G = nx.complete_graph(5)
|
||||
with pytest.raises(nx.NetworkXError, match="Graph is not bipartite"):
|
||||
nx.bipartite_layout(G)
|
||||
# No exception if nodes are explicitly specified
|
||||
pos = nx.bipartite_layout(G, nodes=[2, 3])
|
||||
|
||||
|
||||
def test_bipartite_layout_default_nodes():
|
||||
G = nx.complete_bipartite_graph(3, 3)
|
||||
pos = nx.bipartite_layout(G) # no nodes specified
|
||||
# X coords of nodes should be the same within the bipartite sets
|
||||
for nodeset in nx.bipartite.sets(G):
|
||||
xs = [pos[k][0] for k in nodeset]
|
||||
assert all(x == pytest.approx(xs[0]) for x in xs)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"layout",
|
||||
[
|
||||
nx.random_layout,
|
||||
nx.circular_layout,
|
||||
nx.shell_layout,
|
||||
nx.spring_layout,
|
||||
nx.kamada_kawai_layout,
|
||||
nx.spectral_layout,
|
||||
nx.planar_layout,
|
||||
nx.spiral_layout,
|
||||
nx.forceatlas2_layout,
|
||||
],
|
||||
)
|
||||
def test_layouts_negative_dim(layout):
|
||||
"""Test all layouts that support dim kwarg handle invalid inputs."""
|
||||
G = nx.path_graph(4)
|
||||
valid_err_msgs = "|".join(
|
||||
[
|
||||
"negative dimensions.*not allowed",
|
||||
"can only handle 2",
|
||||
"cannot handle.*2",
|
||||
]
|
||||
)
|
||||
with pytest.raises(ValueError, match=valid_err_msgs):
|
||||
layout(G, dim=-1)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("num_nodes", "expected_method"), [(100, "force"), (501, "energy")]
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"extra_layout_kwargs",
|
||||
[
|
||||
{}, # No extra kwargs
|
||||
{"pos": {0: (0, 0)}, "fixed": [0]}, # Fixed node position
|
||||
{"dim": 3}, # 3D layout
|
||||
],
|
||||
)
|
||||
def test_spring_layout_graph_size_heuristic(
|
||||
num_nodes, expected_method, extra_layout_kwargs
|
||||
):
|
||||
"""Expect 'force' layout for n < 500 and 'energy' for n >= 500"""
|
||||
G = nx.cycle_graph(num_nodes)
|
||||
# Seeded layout to compare explicit method to one determined by "auto"
|
||||
seed = 163674319
|
||||
|
||||
# Compare explicit method to auto method
|
||||
expected = nx.spring_layout(
|
||||
G, method=expected_method, seed=seed, **extra_layout_kwargs
|
||||
)
|
||||
actual = nx.spring_layout(G, method="auto", seed=seed, **extra_layout_kwargs)
|
||||
assert np.allclose(list(expected.values()), list(actual.values()), atol=1e-5)
|
||||
@@ -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
Reference in New Issue
Block a user