| """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) |
|
|
| def test_spring_fixed_without_pos(self): |
| G = nx.path_graph(4) |
| |
| with pytest.raises(ValueError, match="nodes are fixed without positions"): |
| nx.spring_layout(G, fixed=[0]) |
|
|
| pos = {0: (1, 1), 2: (0, 0)} |
| |
| with pytest.raises(ValueError, match="nodes are fixed without positions"): |
| nx.spring_layout(G, fixed=[0, 1], pos=pos) |
|
|
| |
| out = nx.spring_layout(G, fixed=[0, 2], pos=pos) |
| assert all(np.array_equal(out[n], pos[n]) for n in (0, 2)) |
|
|
| def test_spring_init_pos(self): |
| |
| 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)) |
| |
| 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() |
| assert np.linalg.norm(vpos[3]) <= 1 |
| vpos = nx.shell_layout(G, [[0], [1, 2], [3]], rotate=0) |
| assert np.linalg.norm(vpos[3]) <= 1 |
|
|
| 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): |
| |
| 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)]) |
| |
| 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 |
|
|
| |
| pos = np.array([[1.3, -3.2], [2.7, -0.3], [5.1, 2.5]]) |
|
|
| self.check_kamada_kawai_costfn(pos, invdist, meanwt, 2) |
|
|
| |
| 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 |
|
|
| |
| |
| |
| |
| 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) |
|
|
| |
| 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") |
| |
| p = np.array(list(pos.values())) |
| |
| 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): |
| |
| 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): |
| |
| 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): |
| |
| 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) |
|
|
| |
| 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() |
|
|
| |
| assert pos["a"][-1] == pos["d"][-1] |
| |
| assert pos["c"][-1] < pos["a"][-1] < pos["b"][-1] < pos["e"][-1] |
|
|
| |
| G.nodes["a"]["subset"] = "layer_0" |
| pos_nosort = nx.multipartite_layout(G) |
| 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] |
| _, 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) |
| |
| pos = nx.bfs_layout(G, start=0) |
| |
| 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) |
| |
| pos = nx.bfs_layout(G, start=12) |
| assert np.array_equal(_num_nodes_per_bfs_layer(pos), expected_nodes_per_layer) |
| |
| pos = nx.bfs_layout(G, start=6) |
| |
| 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) |
| |
| 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) |
| |
| 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", |
| [ |
| {}, |
| {"pos": {0: (0, 0)}, "fixed": [0]}, |
| {"dim": 3}, |
| ], |
| ) |
| 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) |
| |
| seed = 163674319 |
|
|
| |
| 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) |
|
|