| """ |
| ****** |
| Layout |
| ****** |
| |
| Node positioning algorithms for graph drawing. |
| |
| For `random_layout()` the possible resulting shape |
| is a square of side [0, scale] (default: [0, 1]) |
| Changing `center` shifts the layout by that amount. |
| |
| For the other layout routines, the extent is |
| [center - scale, center + scale] (default: [-1, 1]). |
| |
| Warning: Most layout routines have only been tested in 2-dimensions. |
| |
| """ |
|
|
| import networkx as nx |
| from networkx.utils import np_random_state |
|
|
| __all__ = [ |
| "bipartite_layout", |
| "circular_layout", |
| "forceatlas2_layout", |
| "kamada_kawai_layout", |
| "random_layout", |
| "rescale_layout", |
| "rescale_layout_dict", |
| "shell_layout", |
| "spring_layout", |
| "spectral_layout", |
| "planar_layout", |
| "fruchterman_reingold_layout", |
| "spiral_layout", |
| "multipartite_layout", |
| "bfs_layout", |
| "arf_layout", |
| ] |
|
|
|
|
| def _process_params(G, center, dim): |
| |
| import numpy as np |
|
|
| if not isinstance(G, nx.Graph): |
| empty_graph = nx.Graph() |
| empty_graph.add_nodes_from(G) |
| G = empty_graph |
|
|
| if center is None: |
| center = np.zeros(dim) |
| else: |
| center = np.asarray(center) |
|
|
| if len(center) != dim: |
| msg = "length of center coordinates must match dimension of layout" |
| raise ValueError(msg) |
|
|
| return G, center |
|
|
|
|
| @np_random_state(3) |
| def random_layout(G, center=None, dim=2, seed=None, store_pos_as=None): |
| """Position nodes uniformly at random in the unit square. |
| |
| For every node, a position is generated by choosing each of dim |
| coordinates uniformly at random on the interval [0.0, 1.0). |
| |
| NumPy (http://scipy.org) is required for this function. |
| |
| Parameters |
| ---------- |
| G : NetworkX graph or list of nodes |
| A position will be assigned to every node in G. |
| |
| center : array-like or None |
| Coordinate pair around which to center the layout. |
| |
| dim : int |
| Dimension of layout. |
| |
| seed : int, RandomState instance or None optional (default=None) |
| Set the random state for deterministic node layouts. |
| If int, `seed` is the seed used by the random number generator, |
| if numpy.random.RandomState instance, `seed` is the random |
| number generator, |
| if None, the random number generator is the RandomState instance used |
| by numpy.random. |
| |
| store_pos_as : str, default None |
| If non-None, the position of each node will be stored on the graph as |
| an attribute with this string as its name, which can be accessed with |
| ``G.nodes[...][store_pos_as]``. The function still returns the dictionary. |
| |
| Returns |
| ------- |
| pos : dict |
| A dictionary of positions keyed by node |
| |
| Examples |
| -------- |
| >>> from pprint import pprint |
| >>> G = nx.lollipop_graph(4, 3) |
| >>> pos = nx.random_layout(G) |
| >>> # suppress the returned dict and store on the graph directly |
| >>> _ = nx.random_layout(G, seed=42, store_pos_as="pos") |
| >>> pprint(nx.get_node_attributes(G, "pos")) |
| {0: array([0.37454012, 0.9507143 ], dtype=float32), |
| 1: array([0.7319939, 0.5986585], dtype=float32), |
| 2: array([0.15601864, 0.15599452], dtype=float32), |
| 3: array([0.05808361, 0.8661761 ], dtype=float32), |
| 4: array([0.601115 , 0.7080726], dtype=float32), |
| 5: array([0.02058449, 0.96990985], dtype=float32), |
| 6: array([0.83244264, 0.21233912], dtype=float32)} |
| """ |
| import numpy as np |
|
|
| G, center = _process_params(G, center, dim) |
| pos = seed.rand(len(G), dim) + center |
| pos = pos.astype(np.float32) |
| pos = dict(zip(G, pos)) |
|
|
| if store_pos_as is not None: |
| nx.set_node_attributes(G, pos, store_pos_as) |
| return pos |
|
|
|
|
| def circular_layout(G, scale=1, center=None, dim=2, store_pos_as=None): |
| |
| """Position nodes on a circle. |
| |
| Parameters |
| ---------- |
| G : NetworkX graph or list of nodes |
| A position will be assigned to every node in G. |
| |
| scale : number (default: 1) |
| Scale factor for positions. |
| |
| center : array-like or None |
| Coordinate pair around which to center the layout. |
| |
| dim : int |
| Dimension of layout. |
| If dim>2, the remaining dimensions are set to zero |
| in the returned positions. |
| If dim<2, a ValueError is raised. |
| |
| store_pos_as : str, default None |
| If non-None, the position of each node will be stored on the graph as |
| an attribute with this string as its name, which can be accessed with |
| ``G.nodes[...][store_pos_as]``. The function still returns the dictionary. |
| |
| Returns |
| ------- |
| pos : dict |
| A dictionary of positions keyed by node |
| |
| Raises |
| ------ |
| ValueError |
| If dim < 2 |
| |
| Examples |
| -------- |
| >>> from pprint import pprint |
| >>> G = nx.path_graph(4) |
| >>> pos = nx.circular_layout(G) |
| >>> # suppress the returned dict and store on the graph directly |
| >>> _ = nx.circular_layout(G, store_pos_as="pos") |
| >>> pprint(nx.get_node_attributes(G, "pos")) |
| {0: array([9.99999986e-01, 2.18556937e-08]), |
| 1: array([-3.57647606e-08, 1.00000000e+00]), |
| 2: array([-9.9999997e-01, -6.5567081e-08]), |
| 3: array([ 1.98715071e-08, -9.99999956e-01])} |
| |
| |
| Notes |
| ----- |
| This algorithm currently only works in two dimensions and does not |
| try to minimize edge crossings. |
| |
| """ |
| import numpy as np |
|
|
| if dim < 2: |
| raise ValueError("cannot handle dimensions < 2") |
|
|
| G, center = _process_params(G, center, dim) |
|
|
| paddims = max(0, (dim - 2)) |
|
|
| if len(G) == 0: |
| pos = {} |
| elif len(G) == 1: |
| pos = {nx.utils.arbitrary_element(G): center} |
| else: |
| |
| theta = np.linspace(0, 1, len(G) + 1)[:-1] * 2 * np.pi |
| theta = theta.astype(np.float32) |
| pos = np.column_stack( |
| [np.cos(theta), np.sin(theta), np.zeros((len(G), paddims))] |
| ) |
| pos = rescale_layout(pos, scale=scale) + center |
| pos = dict(zip(G, pos)) |
|
|
| if store_pos_as is not None: |
| nx.set_node_attributes(G, pos, store_pos_as) |
|
|
| return pos |
|
|
|
|
| def shell_layout( |
| G, nlist=None, rotate=None, scale=1, center=None, dim=2, store_pos_as=None |
| ): |
| """Position nodes in concentric circles. |
| |
| Parameters |
| ---------- |
| G : NetworkX graph or list of nodes |
| A position will be assigned to every node in G. |
| |
| nlist : list of lists |
| List of node lists for each shell. |
| |
| rotate : angle in radians (default=pi/len(nlist)) |
| Angle by which to rotate the starting position of each shell |
| relative to the starting position of the previous shell. |
| To recreate behavior before v2.5 use rotate=0. |
| |
| scale : number (default: 1) |
| Scale factor for positions. |
| |
| center : array-like or None |
| Coordinate pair around which to center the layout. |
| |
| dim : int |
| Dimension of layout, currently only dim=2 is supported. |
| Other dimension values result in a ValueError. |
| |
| store_pos_as : str, default None |
| If non-None, the position of each node will be stored on the graph as |
| an attribute with this string as its name, which can be accessed with |
| ``G.nodes[...][store_pos_as]``. The function still returns the dictionary. |
| |
| Returns |
| ------- |
| pos : dict |
| A dictionary of positions keyed by node |
| |
| Raises |
| ------ |
| ValueError |
| If dim != 2 |
| |
| Examples |
| -------- |
| >>> from pprint import pprint |
| >>> G = nx.path_graph(4) |
| >>> shells = [[0], [1, 2, 3]] |
| >>> pos = nx.shell_layout(G, shells) |
| >>> # suppress the returned dict and store on the graph directly |
| >>> _ = nx.shell_layout(G, shells, store_pos_as="pos") |
| >>> pprint(nx.get_node_attributes(G, "pos")) |
| {0: array([0., 0.]), |
| 1: array([-5.00000000e-01, -4.37113883e-08]), |
| 2: array([ 0.24999996, -0.43301272]), |
| 3: array([0.24999981, 0.43301281])} |
| |
| Notes |
| ----- |
| This algorithm currently only works in two dimensions and does not |
| try to minimize edge crossings. |
| |
| """ |
| import numpy as np |
|
|
| if dim != 2: |
| raise ValueError("can only handle 2 dimensions") |
|
|
| G, center = _process_params(G, center, dim) |
|
|
| if len(G) == 0: |
| return {} |
| if len(G) == 1: |
| return {nx.utils.arbitrary_element(G): center} |
|
|
| if nlist is None: |
| |
| nlist = [list(G)] |
|
|
| radius_bump = scale / len(nlist) |
|
|
| if len(nlist[0]) == 1: |
| |
| radius = 0.0 |
| else: |
| |
| radius = radius_bump |
|
|
| if rotate is None: |
| rotate = np.pi / len(nlist) |
| first_theta = rotate |
| npos = {} |
| for nodes in nlist: |
| |
| theta = ( |
| np.linspace(0, 2 * np.pi, len(nodes), endpoint=False, dtype=np.float32) |
| + first_theta |
| ) |
| pos = radius * np.column_stack([np.cos(theta), np.sin(theta)]) + center |
| npos.update(zip(nodes, pos)) |
| radius += radius_bump |
| first_theta += rotate |
|
|
| if store_pos_as is not None: |
| nx.set_node_attributes(G, npos, store_pos_as) |
| return npos |
|
|
|
|
| def bipartite_layout( |
| G, |
| nodes=None, |
| align="vertical", |
| scale=1, |
| center=None, |
| aspect_ratio=4 / 3, |
| store_pos_as=None, |
| ): |
| """Position nodes in two straight lines. |
| |
| Parameters |
| ---------- |
| G : NetworkX graph or list of nodes |
| A position will be assigned to every node in G. |
| |
| nodes : collection of nodes |
| Nodes in one node set of the graph. This set will be placed on |
| left or top. If `None` (the default), a node set is chosen arbitrarily |
| if the graph if bipartite. |
| |
| align : string (default='vertical') |
| The alignment of nodes. Vertical or horizontal. |
| |
| scale : number (default: 1) |
| Scale factor for positions. |
| |
| center : array-like or None |
| Coordinate pair around which to center the layout. |
| |
| aspect_ratio : number (default=4/3): |
| The ratio of the width to the height of the layout. |
| |
| store_pos_as : str, default None |
| If non-None, the position of each node will be stored on the graph as |
| an attribute with this string as its name, which can be accessed with |
| ``G.nodes[...][store_pos_as]``. The function still returns the dictionary. |
| |
| Returns |
| ------- |
| pos : dict |
| A dictionary of positions keyed by node. |
| |
| Raises |
| ------ |
| NetworkXError |
| If ``nodes=None`` and `G` is not bipartite. |
| |
| Examples |
| -------- |
| >>> G = nx.complete_bipartite_graph(3, 3) |
| >>> pos = nx.bipartite_layout(G) |
| |
| The ordering of the layout (i.e. which nodes appear on the left/top) can |
| be specified with the `nodes` parameter: |
| |
| >>> top, bottom = nx.bipartite.sets(G) |
| >>> pos = nx.bipartite_layout(G, nodes=bottom) # "bottom" set appears on the left |
| |
| `store_pos_as` can be used to store the node positions for the computed layout |
| directly on the nodes: |
| |
| >>> _ = nx.bipartite_layout(G, nodes=bottom, store_pos_as="pos") |
| >>> from pprint import pprint |
| >>> pprint(nx.get_node_attributes(G, "pos")) |
| {0: array([ 1. , -0.75]), |
| 1: array([1., 0.]), |
| 2: array([1. , 0.75]), |
| 3: array([-1. , -0.75]), |
| 4: array([-1., 0.]), |
| 5: array([-1. , 0.75])} |
| |
| |
| The ``bipartite_layout`` function can be used with non-bipartite graphs |
| by explicitly specifying how the layout should be partitioned with `nodes`: |
| |
| >>> G = nx.complete_graph(5) # Non-bipartite |
| >>> pos = nx.bipartite_layout(G, nodes={0, 1, 2}) |
| |
| Notes |
| ----- |
| This algorithm currently only works in two dimensions and does not |
| try to minimize edge crossings. |
| |
| """ |
|
|
| import numpy as np |
|
|
| if align not in ("vertical", "horizontal"): |
| msg = "align must be either vertical or horizontal." |
| raise ValueError(msg) |
|
|
| G, center = _process_params(G, center=center, dim=2) |
| if len(G) == 0: |
| return {} |
|
|
| height = 1 |
| width = aspect_ratio * height |
| offset = (width / 2, height / 2) |
|
|
| if nodes is None: |
| top, bottom = nx.bipartite.sets(G) |
| nodes = list(G) |
| else: |
| top = set(nodes) |
| bottom = set(G) - top |
| |
| nodes = list(top) + list(bottom) |
|
|
| left_xs = np.repeat(0, len(top)) |
| right_xs = np.repeat(width, len(bottom)) |
| left_ys = np.linspace(0, height, len(top)) |
| right_ys = np.linspace(0, height, len(bottom)) |
|
|
| top_pos = np.column_stack([left_xs, left_ys]) - offset |
| bottom_pos = np.column_stack([right_xs, right_ys]) - offset |
|
|
| pos = np.concatenate([top_pos, bottom_pos]) |
| pos = rescale_layout(pos, scale=scale) + center |
| if align == "horizontal": |
| pos = pos[:, ::-1] |
| pos = dict(zip(nodes, pos)) |
|
|
| if store_pos_as is not None: |
| nx.set_node_attributes(G, pos, store_pos_as) |
|
|
| return pos |
|
|
|
|
| @np_random_state("seed") |
| def spring_layout( |
| G, |
| k=None, |
| pos=None, |
| fixed=None, |
| iterations=50, |
| threshold=1e-4, |
| weight="weight", |
| scale=1, |
| center=None, |
| dim=2, |
| seed=None, |
| store_pos_as=None, |
| *, |
| method="auto", |
| gravity=1.0, |
| ): |
| """Position nodes using Fruchterman-Reingold force-directed algorithm. |
| |
| The algorithm simulates a force-directed representation of the network |
| treating edges as springs holding nodes close, while treating nodes |
| as repelling objects, sometimes called an anti-gravity force. |
| Simulation continues until the positions are close to an equilibrium. |
| |
| There are some hard-coded values: minimal distance between |
| nodes (0.01) and "temperature" of 0.1 to ensure nodes don't fly away. |
| During the simulation, `k` helps determine the distance between nodes, |
| though `scale` and `center` determine the size and place after |
| rescaling occurs at the end of the simulation. |
| |
| Fixing some nodes doesn't allow them to move in the simulation. |
| It also turns off the rescaling feature at the simulation's end. |
| In addition, setting `scale` to `None` turns off rescaling. |
| |
| Parameters |
| ---------- |
| G : NetworkX graph or list of nodes |
| A position will be assigned to every node in G. |
| |
| k : float (default=None) |
| Optimal distance between nodes. If None the distance is set to |
| 1/sqrt(n) where n is the number of nodes. Increase this value |
| to move nodes farther apart. |
| |
| pos : dict or None optional (default=None) |
| Initial positions for nodes as a dictionary with node as keys |
| and values as a coordinate list or tuple. If None, then use |
| random initial positions. |
| |
| fixed : list or None optional (default=None) |
| Nodes to keep fixed at initial position. |
| Nodes not in ``G.nodes`` are ignored. |
| ValueError raised if `fixed` specified and `pos` not. |
| |
| iterations : int optional (default=50) |
| Maximum number of iterations taken |
| |
| threshold: float optional (default = 1e-4) |
| Threshold for relative error in node position changes. |
| The iteration stops if the error is below this threshold. |
| |
| weight : string or None optional (default='weight') |
| The edge attribute that holds the numerical value used for |
| the edge weight. Larger means a stronger attractive force. |
| If None, then all edge weights are 1. |
| |
| scale : number or None (default: 1) |
| Scale factor for positions. Not used unless `fixed is None`. |
| If scale is None, no rescaling is performed. |
| |
| center : array-like or None |
| Coordinate pair around which to center the layout. |
| Not used unless `fixed is None`. |
| |
| dim : int |
| Dimension of layout. |
| |
| seed : int, RandomState instance or None optional (default=None) |
| Used only for the initial positions in the algorithm. |
| Set the random state for deterministic node layouts. |
| If int, `seed` is the seed used by the random number generator, |
| if numpy.random.RandomState instance, `seed` is the random |
| number generator, |
| if None, the random number generator is the RandomState instance used |
| by numpy.random. |
| |
| store_pos_as : str, default None |
| If non-None, the position of each node will be stored on the graph as |
| an attribute with this string as its name, which can be accessed with |
| ``G.nodes[...][store_pos_as]``. The function still returns the dictionary. |
| |
| method : str optional (default='auto') |
| The method to compute the layout. |
| If 'force', the force-directed Fruchterman-Reingold algorithm [1]_ is used. |
| If 'energy', the energy-based optimization algorithm [2]_ is used with absolute |
| values of edge weights and gravitational forces acting on each connected component. |
| If 'auto', we use 'force' if ``len(G) < 500`` and 'energy' otherwise. |
| |
| gravity: float optional (default=1.0) |
| Used only for the method='energy'. |
| The positive coefficient of gravitational forces per connected component. |
| |
| Returns |
| ------- |
| pos : dict |
| A dictionary of positions keyed by node |
| |
| Examples |
| -------- |
| >>> from pprint import pprint |
| >>> G = nx.path_graph(4) |
| >>> pos = nx.spring_layout(G) |
| >>> # suppress the returned dict and store on the graph directly |
| >>> _ = nx.spring_layout(G, seed=123, store_pos_as="pos") |
| >>> pprint(nx.get_node_attributes(G, "pos")) |
| {0: array([-0.61495802, -1. ]), |
| 1: array([-0.21789544, -0.35432583]), |
| 2: array([0.21847843, 0.35527369]), |
| 3: array([0.61437502, 0.99905215])} |
| |
| |
| # The same using longer but equivalent function name |
| >>> pos = nx.fruchterman_reingold_layout(G) |
| |
| References |
| ---------- |
| .. [1] Fruchterman, Thomas MJ, and Edward M. Reingold. |
| "Graph drawing by force-directed placement." |
| Software: Practice and experience 21, no. 11 (1991): 1129-1164. |
| http://dx.doi.org/10.1002/spe.4380211102 |
| .. [2] Hamaguchi, Hiroki, Naoki Marumo, and Akiko Takeda. |
| "Initial Placement for Fruchterman--Reingold Force Model With Coordinate Newton Direction." |
| arXiv preprint arXiv:2412.20317 (2024). |
| https://arxiv.org/abs/2412.20317 |
| """ |
| import numpy as np |
|
|
| if method not in ("auto", "force", "energy"): |
| raise ValueError("the method must be either auto, force, or energy.") |
| if method == "auto": |
| method = "force" if len(G) < 500 else "energy" |
|
|
| G, center = _process_params(G, center, dim) |
|
|
| if fixed is not None: |
| if pos is None: |
| raise ValueError("nodes are fixed without positions given") |
| for node in fixed: |
| if node not in pos: |
| raise ValueError("nodes are fixed without positions given") |
| nfixed = {node: i for i, node in enumerate(G)} |
| fixed = np.asarray([nfixed[node] for node in fixed if node in nfixed]) |
|
|
| if pos is not None: |
| |
| dom_size = max(coord for pos_tup in pos.values() for coord in pos_tup) |
| if dom_size == 0: |
| dom_size = 1 |
| pos_arr = seed.rand(len(G), dim) * dom_size + center |
|
|
| for i, n in enumerate(G): |
| if n in pos: |
| pos_arr[i] = np.asarray(pos[n]) |
| else: |
| pos_arr = None |
| dom_size = 1 |
|
|
| if len(G) == 0: |
| return {} |
| if len(G) == 1: |
| pos = {nx.utils.arbitrary_element(G.nodes()): center} |
| if store_pos_as is not None: |
| nx.set_node_attributes(G, pos, store_pos_as) |
| return pos |
|
|
| |
| if len(G) >= 500 or method == "energy": |
| A = nx.to_scipy_sparse_array(G, weight=weight, dtype="f") |
| if k is None and fixed is not None: |
| |
| nnodes, _ = A.shape |
| k = dom_size / np.sqrt(nnodes) |
| pos = _sparse_fruchterman_reingold( |
| A, k, pos_arr, fixed, iterations, threshold, dim, seed, method, gravity |
| ) |
| else: |
| A = nx.to_numpy_array(G, weight=weight) |
| if k is None and fixed is not None: |
| |
| nnodes, _ = A.shape |
| k = dom_size / np.sqrt(nnodes) |
| pos = _fruchterman_reingold( |
| A, k, pos_arr, fixed, iterations, threshold, dim, seed |
| ) |
| if fixed is None and scale is not None: |
| pos = rescale_layout(pos, scale=scale) + center |
| pos = dict(zip(G, pos)) |
|
|
| if store_pos_as is not None: |
| nx.set_node_attributes(G, pos, store_pos_as) |
|
|
| return pos |
|
|
|
|
| fruchterman_reingold_layout = spring_layout |
|
|
|
|
| @np_random_state(7) |
| def _fruchterman_reingold( |
| A, k=None, pos=None, fixed=None, iterations=50, threshold=1e-4, dim=2, seed=None |
| ): |
| |
| |
| import numpy as np |
|
|
| try: |
| nnodes, _ = A.shape |
| except AttributeError as err: |
| msg = "fruchterman_reingold() takes an adjacency matrix as input" |
| raise nx.NetworkXError(msg) from err |
|
|
| if pos is None: |
| |
| pos = np.asarray(seed.rand(nnodes, dim), dtype=A.dtype) |
| else: |
| |
| pos = pos.astype(A.dtype) |
|
|
| |
| if k is None: |
| k = np.sqrt(1.0 / nnodes) |
| |
| |
| |
| |
| t = max(max(pos.T[0]) - min(pos.T[0]), max(pos.T[1]) - min(pos.T[1])) * 0.1 |
| |
| |
| dt = t / (iterations + 1) |
| delta = np.zeros((pos.shape[0], pos.shape[0], pos.shape[1]), dtype=A.dtype) |
| |
| |
| |
| for iteration in range(iterations): |
| |
| delta = pos[:, np.newaxis, :] - pos[np.newaxis, :, :] |
| |
| distance = np.linalg.norm(delta, axis=-1) |
| |
| np.clip(distance, 0.01, None, out=distance) |
| |
| displacement = np.einsum( |
| "ijk,ij->ik", delta, (k * k / distance**2 - A * distance / k) |
| ) |
| |
| length = np.linalg.norm(displacement, axis=-1) |
| |
| |
| length = np.clip(length, a_min=0.01, a_max=None) |
| delta_pos = np.einsum("ij,i->ij", displacement, t / length) |
| if fixed is not None: |
| |
| delta_pos[fixed] = 0.0 |
| pos += delta_pos |
| |
| t -= dt |
| if (np.linalg.norm(delta_pos) / nnodes) < threshold: |
| break |
| return pos |
|
|
|
|
| @np_random_state(7) |
| def _sparse_fruchterman_reingold( |
| A, |
| k=None, |
| pos=None, |
| fixed=None, |
| iterations=50, |
| threshold=1e-4, |
| dim=2, |
| seed=None, |
| method="energy", |
| gravity=1.0, |
| ): |
| |
| |
| |
| import numpy as np |
| import scipy as sp |
|
|
| try: |
| nnodes, _ = A.shape |
| except AttributeError as err: |
| msg = "fruchterman_reingold() takes an adjacency matrix as input" |
| raise nx.NetworkXError(msg) from err |
|
|
| if pos is None: |
| |
| pos = np.asarray(seed.rand(nnodes, dim), dtype=A.dtype) |
| else: |
| |
| pos = pos.astype(A.dtype) |
|
|
| |
| if fixed is None: |
| fixed = [] |
|
|
| |
| if k is None: |
| k = np.sqrt(1.0 / nnodes) |
|
|
| if method == "energy": |
| return _energy_fruchterman_reingold( |
| A, nnodes, k, pos, fixed, iterations, threshold, dim, gravity |
| ) |
|
|
| |
| try: |
| A = A.tolil() |
| except AttributeError: |
| A = (sp.sparse.coo_array(A)).tolil() |
|
|
| |
| |
| t = max(max(pos.T[0]) - min(pos.T[0]), max(pos.T[1]) - min(pos.T[1])) * 0.1 |
| |
| |
| dt = t / (iterations + 1) |
|
|
| displacement = np.zeros((dim, nnodes)) |
| for iteration in range(iterations): |
| displacement *= 0 |
| |
| for i in range(A.shape[0]): |
| if i in fixed: |
| continue |
| |
| delta = (pos[i] - pos).T |
| |
| distance = np.sqrt((delta**2).sum(axis=0)) |
| |
| distance = np.clip(distance, a_min=0.01, a_max=None) |
| |
| Ai = A.getrowview(i).toarray() |
| |
| displacement[:, i] += ( |
| delta * (k * k / distance**2 - Ai * distance / k) |
| ).sum(axis=1) |
| |
| length = np.sqrt((displacement**2).sum(axis=0)) |
| |
| |
| length = np.clip(length, a_min=0.01, a_max=None) |
| delta_pos = (displacement * t / length).T |
| pos += delta_pos |
| |
| t -= dt |
| if (np.linalg.norm(delta_pos) / nnodes) < threshold: |
| break |
| return pos |
|
|
|
|
| def _energy_fruchterman_reingold( |
| A, nnodes, k, pos, fixed, iterations, threshold, dim, gravity |
| ): |
| |
| |
| import numpy as np |
| import scipy as sp |
|
|
| if gravity <= 0: |
| raise ValueError(f"the gravity must be positive.") |
|
|
| |
| try: |
| A = A.tocsr() |
| except AttributeError: |
| A = sp.sparse.csr_array(A) |
|
|
| |
| A = np.abs(A) |
| A = (A + A.T) / 2 |
|
|
| n_components, labels = sp.sparse.csgraph.connected_components(A, directed=False) |
| bincount = np.bincount(labels) |
| batchsize = 500 |
|
|
| def _cost_FR(x): |
| pos = x.reshape((nnodes, dim)) |
| grad = np.zeros((nnodes, dim)) |
| cost = 0.0 |
| for l in range(0, nnodes, batchsize): |
| r = min(l + batchsize, nnodes) |
| |
| delta = pos[l:r, np.newaxis, :] - pos[np.newaxis, :, :] |
| |
| distance2 = np.sum(delta * delta, axis=2) |
| distance2 = np.maximum(distance2, 1e-10) |
| distance = np.sqrt(distance2) |
| |
| Ad = A[l:r] * distance |
| |
| grad[l:r] = 2 * np.einsum("ij,ijk->ik", Ad / k - k**2 / distance2, delta) |
| |
| cost += np.sum(Ad * distance2) / (3 * k) |
| |
| cost -= k**2 * np.sum(np.log(distance)) |
| |
| centers = np.zeros((n_components, dim)) |
| np.add.at(centers, labels, pos) |
| delta0 = centers / bincount[:, np.newaxis] - 0.5 |
| grad += gravity * delta0[labels] |
| cost += gravity * 0.5 * np.sum(bincount * np.linalg.norm(delta0, axis=1) ** 2) |
| |
| grad[fixed] = 0.0 |
| return cost, grad.ravel() |
|
|
| |
| options = {"maxiter": iterations, "gtol": threshold} |
| return sp.optimize.minimize( |
| _cost_FR, pos.ravel(), method="L-BFGS-B", jac=True, options=options |
| ).x.reshape((nnodes, dim)) |
|
|
|
|
| def kamada_kawai_layout( |
| G, |
| dist=None, |
| pos=None, |
| weight="weight", |
| scale=1, |
| center=None, |
| dim=2, |
| store_pos_as=None, |
| ): |
| """Position nodes using Kamada-Kawai path-length cost-function. |
| |
| Parameters |
| ---------- |
| G : NetworkX graph or list of nodes |
| A position will be assigned to every node in G. |
| |
| dist : dict (default=None) |
| A two-level dictionary of optimal distances between nodes, |
| indexed by source and destination node. |
| If None, the distance is computed using shortest_path_length(). |
| |
| pos : dict or None optional (default=None) |
| Initial positions for nodes as a dictionary with node as keys |
| and values as a coordinate list or tuple. If None, then use |
| circular_layout() for dim >= 2 and a linear layout for dim == 1. |
| |
| weight : string or None optional (default='weight') |
| The edge attribute that holds the numerical value used for |
| the edge weight. If None, then all edge weights are 1. |
| |
| scale : number (default: 1) |
| Scale factor for positions. |
| |
| center : array-like or None |
| Coordinate pair around which to center the layout. |
| |
| dim : int |
| Dimension of layout. |
| |
| store_pos_as : str, default None |
| If non-None, the position of each node will be stored on the graph as |
| an attribute with this string as its name, which can be accessed with |
| ``G.nodes[...][store_pos_as]``. The function still returns the dictionary. |
| |
| Returns |
| ------- |
| pos : dict |
| A dictionary of positions keyed by node |
| |
| Examples |
| -------- |
| >>> from pprint import pprint |
| >>> G = nx.path_graph(4) |
| >>> pos = nx.kamada_kawai_layout(G) |
| >>> # suppress the returned dict and store on the graph directly |
| >>> _ = nx.kamada_kawai_layout(G, store_pos_as="pos") |
| >>> pprint(nx.get_node_attributes(G, "pos")) |
| {0: array([0.99996577, 0.99366857]), |
| 1: array([0.32913544, 0.33543827]), |
| 2: array([-0.33544334, -0.32910684]), |
| 3: array([-0.99365787, -1. ])} |
| """ |
| import numpy as np |
|
|
| G, center = _process_params(G, center, dim) |
| nNodes = len(G) |
| if nNodes == 0: |
| return {} |
|
|
| if dist is None: |
| dist = dict(nx.shortest_path_length(G, weight=weight)) |
| dist_mtx = 1e6 * np.ones((nNodes, nNodes)) |
| for row, nr in enumerate(G): |
| if nr not in dist: |
| continue |
| rdist = dist[nr] |
| for col, nc in enumerate(G): |
| if nc not in rdist: |
| continue |
| dist_mtx[row][col] = rdist[nc] |
|
|
| if pos is None: |
| if dim >= 3: |
| pos = random_layout(G, dim=dim) |
| elif dim == 2: |
| pos = circular_layout(G, dim=dim) |
| else: |
| pos = dict(zip(G, np.linspace(0, 1, len(G)))) |
| pos_arr = np.array([pos[n] for n in G]) |
|
|
| pos = _kamada_kawai_solve(dist_mtx, pos_arr, dim) |
|
|
| pos = rescale_layout(pos, scale=scale) + center |
| pos = dict(zip(G, pos)) |
|
|
| if store_pos_as is not None: |
| nx.set_node_attributes(G, pos, store_pos_as) |
|
|
| return pos |
|
|
|
|
| def _kamada_kawai_solve(dist_mtx, pos_arr, dim): |
| |
| |
| |
|
|
| import numpy as np |
| import scipy as sp |
|
|
| meanwt = 1e-3 |
| costargs = (np, 1 / (dist_mtx + np.eye(dist_mtx.shape[0]) * 1e-3), meanwt, dim) |
|
|
| optresult = sp.optimize.minimize( |
| _kamada_kawai_costfn, |
| pos_arr.ravel(), |
| method="L-BFGS-B", |
| args=costargs, |
| jac=True, |
| ) |
|
|
| return optresult.x.reshape((-1, dim)) |
|
|
|
|
| def _kamada_kawai_costfn(pos_vec, np, invdist, meanweight, dim): |
| |
| nNodes = invdist.shape[0] |
| pos_arr = pos_vec.reshape((nNodes, dim)) |
|
|
| delta = pos_arr[:, np.newaxis, :] - pos_arr[np.newaxis, :, :] |
| nodesep = np.linalg.norm(delta, axis=-1) |
| direction = np.einsum("ijk,ij->ijk", delta, 1 / (nodesep + np.eye(nNodes) * 1e-3)) |
|
|
| offset = nodesep * invdist - 1.0 |
| offset[np.diag_indices(nNodes)] = 0 |
|
|
| cost = 0.5 * np.sum(offset**2) |
| grad = np.einsum("ij,ij,ijk->ik", invdist, offset, direction) - np.einsum( |
| "ij,ij,ijk->jk", invdist, offset, direction |
| ) |
|
|
| |
| sumpos = np.sum(pos_arr, axis=0) |
| cost += 0.5 * meanweight * np.sum(sumpos**2) |
| grad += meanweight * sumpos |
|
|
| return (cost, grad.ravel()) |
|
|
|
|
| def spectral_layout(G, weight="weight", scale=1, center=None, dim=2, store_pos_as=None): |
| """Position nodes using the eigenvectors of the graph Laplacian. |
| |
| Using the unnormalized Laplacian, the layout shows possible clusters of |
| nodes which are an approximation of the ratio cut. If dim is the number of |
| dimensions then the positions are the entries of the dim eigenvectors |
| corresponding to the ascending eigenvalues starting from the second one. |
| |
| Parameters |
| ---------- |
| G : NetworkX graph or list of nodes |
| A position will be assigned to every node in G. |
| |
| weight : string or None optional (default='weight') |
| The edge attribute that holds the numerical value used for |
| the edge weight. If None, then all edge weights are 1. |
| |
| scale : number (default: 1) |
| Scale factor for positions. |
| |
| center : array-like or None |
| Coordinate pair around which to center the layout. |
| |
| dim : int |
| Dimension of layout. |
| |
| store_pos_as : str, default None |
| If non-None, the position of each node will be stored on the graph as |
| an attribute with this string as its name, which can be accessed with |
| ``G.nodes[...][store_pos_as]``. The function still returns the dictionary. |
| |
| Returns |
| ------- |
| pos : dict |
| A dictionary of positions keyed by node |
| |
| Examples |
| -------- |
| >>> from pprint import pprint |
| >>> G = nx.path_graph(4) |
| >>> pos = nx.spectral_layout(G) |
| >>> # suppress the returned dict and store on the graph directly |
| >>> _ = nx.spectral_layout(G, store_pos_as="pos") |
| >>> pprint(nx.get_node_attributes(G, "pos")) |
| {0: array([-1. , 0.76536686]), |
| 1: array([-0.41421356, -0.76536686]), |
| 2: array([ 0.41421356, -0.76536686]), |
| 3: array([1. , 0.76536686])} |
| |
| |
| Notes |
| ----- |
| Directed graphs will be considered as undirected graphs when |
| positioning the nodes. |
| |
| For larger graphs (>500 nodes) this will use the SciPy sparse |
| eigenvalue solver (ARPACK). |
| """ |
| |
| import numpy as np |
|
|
| G, center = _process_params(G, center, dim) |
|
|
| if len(G) <= 2: |
| if len(G) == 0: |
| pos = np.array([]) |
| elif len(G) == 1: |
| pos = np.array([center]) |
| else: |
| pos = np.array([np.zeros(dim), np.array(center) * 2.0]) |
| return dict(zip(G, pos)) |
| try: |
| |
| if len(G) < 500: |
| raise ValueError |
| A = nx.to_scipy_sparse_array(G, weight=weight, dtype="d") |
| |
| if G.is_directed(): |
| A = A + np.transpose(A) |
| pos = _sparse_spectral(A, dim) |
| except (ImportError, ValueError): |
| |
| A = nx.to_numpy_array(G, weight=weight) |
| |
| if G.is_directed(): |
| A += A.T |
| pos = _spectral(A, dim) |
|
|
| pos = rescale_layout(pos, scale=scale) + center |
| pos = dict(zip(G, pos)) |
|
|
| if store_pos_as is not None: |
| nx.set_node_attributes(G, pos, store_pos_as) |
|
|
| return pos |
|
|
|
|
| def _spectral(A, dim=2): |
| |
| |
| import numpy as np |
|
|
| try: |
| nnodes, _ = A.shape |
| except AttributeError as err: |
| msg = "spectral() takes an adjacency matrix as input" |
| raise nx.NetworkXError(msg) from err |
|
|
| |
| D = np.identity(nnodes, dtype=A.dtype) * np.sum(A, axis=1) |
| L = D - A |
|
|
| eigenvalues, eigenvectors = np.linalg.eig(L) |
| |
| index = np.argsort(eigenvalues)[1 : dim + 1] |
| return np.real(eigenvectors[:, index]) |
|
|
|
|
| def _sparse_spectral(A, dim=2): |
| |
| |
| |
| import numpy as np |
| import scipy as sp |
|
|
| try: |
| nnodes, _ = A.shape |
| except AttributeError as err: |
| msg = "sparse_spectral() takes an adjacency matrix as input" |
| raise nx.NetworkXError(msg) from err |
|
|
| |
| D = sp.sparse.dia_array((A.sum(axis=1), 0), shape=(nnodes, nnodes)).tocsr() |
| L = D - A |
|
|
| k = dim + 1 |
| |
| ncv = max(2 * k + 1, int(np.sqrt(nnodes))) |
| |
| eigenvalues, eigenvectors = sp.sparse.linalg.eigsh(L, k, which="SM", ncv=ncv) |
| index = np.argsort(eigenvalues)[1:k] |
| return np.real(eigenvectors[:, index]) |
|
|
|
|
| def planar_layout(G, scale=1, center=None, dim=2, store_pos_as=None): |
| """Position nodes without edge intersections. |
| |
| Parameters |
| ---------- |
| G : NetworkX graph or list of nodes |
| A position will be assigned to every node in G. If G is of type |
| nx.PlanarEmbedding, the positions are selected accordingly. |
| |
| scale : number (default: 1) |
| Scale factor for positions. |
| |
| center : array-like or None |
| Coordinate pair around which to center the layout. |
| |
| dim : int |
| Dimension of layout. |
| |
| store_pos_as : str, default None |
| If non-None, the position of each node will be stored on the graph as |
| an attribute with this string as its name, which can be accessed with |
| ``G.nodes[...][store_pos_as]``. The function still returns the dictionary. |
| |
| Returns |
| ------- |
| pos : dict |
| A dictionary of positions keyed by node |
| |
| Raises |
| ------ |
| NetworkXException |
| If G is not planar |
| |
| Examples |
| -------- |
| >>> from pprint import pprint |
| >>> G = nx.path_graph(4) |
| >>> pos = nx.planar_layout(G) |
| >>> # suppress the returned dict and store on the graph directly |
| >>> _ = nx.planar_layout(G, store_pos_as="pos") |
| >>> pprint(nx.get_node_attributes(G, "pos")) |
| {0: array([-0.77777778, -0.33333333]), |
| 1: array([ 1. , -0.33333333]), |
| 2: array([0.11111111, 0.55555556]), |
| 3: array([-0.33333333, 0.11111111])} |
| """ |
| import numpy as np |
|
|
| if dim != 2: |
| raise ValueError("can only handle 2 dimensions") |
|
|
| G, center = _process_params(G, center, dim) |
|
|
| if len(G) == 0: |
| return {} |
|
|
| if isinstance(G, nx.PlanarEmbedding): |
| embedding = G |
| else: |
| is_planar, embedding = nx.check_planarity(G) |
| if not is_planar: |
| raise nx.NetworkXException("G is not planar.") |
| pos = nx.combinatorial_embedding_to_pos(embedding) |
| node_list = list(embedding) |
| pos = np.vstack([pos[x] for x in node_list]) |
| pos = pos.astype(np.float64) |
| pos = rescale_layout(pos, scale=scale) + center |
| pos = dict(zip(node_list, pos)) |
| if store_pos_as is not None: |
| nx.set_node_attributes(G, pos, store_pos_as) |
| return pos |
|
|
|
|
| def spiral_layout( |
| G, |
| scale=1, |
| center=None, |
| dim=2, |
| resolution=0.35, |
| equidistant=False, |
| store_pos_as=None, |
| ): |
| """Position nodes in a spiral layout. |
| |
| Parameters |
| ---------- |
| G : NetworkX graph or list of nodes |
| A position will be assigned to every node in G. |
| |
| scale : number (default: 1) |
| Scale factor for positions. |
| |
| center : array-like or None |
| Coordinate pair around which to center the layout. |
| |
| dim : int, default=2 |
| Dimension of layout, currently only dim=2 is supported. |
| Other dimension values result in a ValueError. |
| |
| resolution : float, default=0.35 |
| The compactness of the spiral layout returned. |
| Lower values result in more compressed spiral layouts. |
| |
| equidistant : bool, default=False |
| If True, nodes will be positioned equidistant from each other |
| by decreasing angle further from center. |
| If False, nodes will be positioned at equal angles |
| from each other by increasing separation further from center. |
| |
| store_pos_as : str, default None |
| If non-None, the position of each node will be stored on the graph as |
| an attribute with this string as its name, which can be accessed with |
| ``G.nodes[...][store_pos_as]``. The function still returns the dictionary. |
| |
| Returns |
| ------- |
| pos : dict |
| A dictionary of positions keyed by node |
| |
| Raises |
| ------ |
| ValueError |
| If dim != 2 |
| |
| Examples |
| -------- |
| >>> from pprint import pprint |
| >>> G = nx.path_graph(4) |
| >>> pos = nx.spiral_layout(G) |
| >>> nx.draw(G, pos=pos) |
| >>> # suppress the returned dict and store on the graph directly |
| >>> _ = nx.spiral_layout(G, store_pos_as="pos") |
| >>> pprint(nx.get_node_attributes(G, "pos")) |
| {0: array([-0.64153279, -0.68555087]), |
| 1: array([-0.03307913, -0.46344795]), |
| 2: array([0.34927952, 0.14899882]), |
| 3: array([0.32533239, 1. ])} |
| |
| Notes |
| ----- |
| This algorithm currently only works in two dimensions. |
| |
| """ |
| import numpy as np |
|
|
| if dim != 2: |
| raise ValueError("can only handle 2 dimensions") |
|
|
| G, center = _process_params(G, center, dim) |
|
|
| if len(G) == 0: |
| return {} |
| if len(G) == 1: |
| pos = {nx.utils.arbitrary_element(G): center} |
| if store_pos_as is not None: |
| nx.set_node_attributes(G, pos, store_pos_as) |
| return pos |
|
|
| pos = [] |
| if equidistant: |
| chord = 1 |
| step = 0.5 |
| theta = resolution |
| theta += chord / (step * theta) |
| for _ in range(len(G)): |
| r = step * theta |
| theta += chord / r |
| pos.append([np.cos(theta) * r, np.sin(theta) * r]) |
|
|
| else: |
| dist = np.arange(len(G), dtype=float) |
| angle = resolution * dist |
| pos = np.transpose(dist * np.array([np.cos(angle), np.sin(angle)])) |
|
|
| pos = rescale_layout(np.array(pos), scale=scale) + center |
|
|
| pos = dict(zip(G, pos)) |
|
|
| if store_pos_as is not None: |
| nx.set_node_attributes(G, pos, store_pos_as) |
|
|
| return pos |
|
|
|
|
| def multipartite_layout( |
| G, subset_key="subset", align="vertical", scale=1, center=None, store_pos_as=None |
| ): |
| """Position nodes in layers of straight lines. |
| |
| Parameters |
| ---------- |
| G : NetworkX graph or list of nodes |
| A position will be assigned to every node in G. |
| |
| subset_key : string or dict (default='subset') |
| If a string, the key of node data in G that holds the node subset. |
| If a dict, keyed by layer number to the nodes in that layer/subset. |
| |
| align : string (default='vertical') |
| The alignment of nodes. Vertical or horizontal. |
| |
| scale : number (default: 1) |
| Scale factor for positions. |
| |
| center : array-like or None |
| Coordinate pair around which to center the layout. |
| |
| store_pos_as : str, default None |
| If non-None, the position of each node will be stored on the graph as |
| an attribute with this string as its name, which can be accessed with |
| ``G.nodes[...][store_pos_as]``. The function still returns the dictionary. |
| |
| Returns |
| ------- |
| pos : dict |
| A dictionary of positions keyed by node. |
| |
| Examples |
| -------- |
| >>> G = nx.complete_multipartite_graph(28, 16, 10) |
| >>> pos = nx.multipartite_layout(G) |
| >>> # suppress the returned dict and store on the graph directly |
| >>> G = nx.complete_multipartite_graph(28, 16, 10) |
| >>> _ = nx.multipartite_layout(G, store_pos_as="pos") |
| |
| or use a dict to provide the layers of the layout |
| |
| >>> G = nx.Graph([(0, 1), (1, 2), (1, 3), (3, 4)]) |
| >>> layers = {"a": [0], "b": [1], "c": [2, 3], "d": [4]} |
| >>> pos = nx.multipartite_layout(G, subset_key=layers) |
| |
| Notes |
| ----- |
| This algorithm currently only works in two dimensions and does not |
| try to minimize edge crossings. |
| |
| Network does not need to be a complete multipartite graph. As long as nodes |
| have subset_key data, they will be placed in the corresponding layers. |
| |
| """ |
| import numpy as np |
|
|
| if align not in ("vertical", "horizontal"): |
| msg = "align must be either vertical or horizontal." |
| raise ValueError(msg) |
|
|
| G, center = _process_params(G, center=center, dim=2) |
| if len(G) == 0: |
| return {} |
|
|
| try: |
| |
| if len(G) != sum(len(nodes) for nodes in subset_key.values()): |
| raise nx.NetworkXError( |
| "all nodes must be in one subset of `subset_key` dict" |
| ) |
| except AttributeError: |
| |
| node_to_subset = nx.get_node_attributes(G, subset_key) |
| if len(node_to_subset) != len(G): |
| raise nx.NetworkXError( |
| f"all nodes need a subset_key attribute: {subset_key}" |
| ) |
| subset_key = nx.utils.groups(node_to_subset) |
|
|
| |
| try: |
| layers = dict(sorted(subset_key.items())) |
| except TypeError: |
| layers = subset_key |
|
|
| pos = None |
| nodes = [] |
| width = len(layers) |
| for i, layer in enumerate(layers.values()): |
| height = len(layer) |
| xs = np.repeat(i, height) |
| ys = np.arange(0, height, dtype=float) |
| offset = ((width - 1) / 2, (height - 1) / 2) |
| layer_pos = np.column_stack([xs, ys]) - offset |
| if pos is None: |
| pos = layer_pos |
| else: |
| pos = np.concatenate([pos, layer_pos]) |
| nodes.extend(layer) |
| pos = rescale_layout(pos, scale=scale) + center |
| if align == "horizontal": |
| pos = pos[:, ::-1] |
| pos = dict(zip(nodes, pos)) |
|
|
| if store_pos_as is not None: |
| nx.set_node_attributes(G, pos, store_pos_as) |
|
|
| return pos |
|
|
|
|
| @np_random_state("seed") |
| def arf_layout( |
| G, |
| pos=None, |
| scaling=1, |
| a=1.1, |
| etol=1e-6, |
| dt=1e-3, |
| max_iter=1000, |
| *, |
| seed=None, |
| store_pos_as=None, |
| ): |
| """Arf layout for networkx |
| |
| The attractive and repulsive forces (arf) layout [1] improves the spring |
| layout in three ways. First, it prevents congestion of highly connected nodes |
| due to strong forcing between nodes. Second, it utilizes the layout space |
| more effectively by preventing large gaps that spring layout tends to create. |
| Lastly, the arf layout represents symmetries in the layout better than the |
| default spring layout. |
| |
| Parameters |
| ---------- |
| G : nx.Graph or nx.DiGraph |
| Networkx graph. |
| pos : dict |
| Initial position of the nodes. If set to None a |
| random layout will be used. |
| scaling : float |
| Scales the radius of the circular layout space. |
| a : float |
| Strength of springs between connected nodes. Should be larger than 1. |
| The greater a, the clearer the separation of unconnected sub clusters. |
| etol : float |
| Gradient sum of spring forces must be larger than `etol` before successful |
| termination. |
| dt : float |
| Time step for force differential equation simulations. |
| max_iter : int |
| Max iterations before termination of the algorithm. |
| seed : int, RandomState instance or None optional (default=None) |
| Set the random state for deterministic node layouts. |
| If int, `seed` is the seed used by the random number generator, |
| if numpy.random.RandomState instance, `seed` is the random |
| number generator, |
| if None, the random number generator is the RandomState instance used |
| by numpy.random. |
| store_pos_as : str, default None |
| If non-None, the position of each node will be stored on the graph as |
| an attribute with this string as its name, which can be accessed with |
| ``G.nodes[...][store_pos_as]``. The function still returns the dictionary. |
| |
| Returns |
| ------- |
| pos : dict |
| A dictionary of positions keyed by node. |
| |
| Examples |
| -------- |
| >>> G = nx.grid_graph((5, 5)) |
| >>> pos = nx.arf_layout(G) |
| >>> # suppress the returned dict and store on the graph directly |
| >>> G = nx.grid_graph((5, 5)) |
| >>> _ = nx.arf_layout(G, store_pos_as="pos") |
| |
| References |
| ---------- |
| .. [1] "Self-Organization Applied to Dynamic Network Layout", M. Geipel, |
| International Journal of Modern Physics C, 2007, Vol 18, No 10, |
| pp. 1537-1549. |
| https://doi.org/10.1142/S0129183107011558 https://arxiv.org/abs/0704.1748 |
| """ |
| import warnings |
|
|
| import numpy as np |
|
|
| if a <= 1: |
| msg = "The parameter a should be larger than 1" |
| raise ValueError(msg) |
|
|
| pos_tmp = nx.random_layout(G, seed=seed) |
| if pos is None: |
| pos = pos_tmp |
| else: |
| for node in G.nodes(): |
| if node not in pos: |
| pos[node] = pos_tmp[node].copy() |
|
|
| |
| N = len(G) |
| |
| if N == 0: |
| return pos |
|
|
| |
| K = np.ones((N, N)) - np.eye(N) |
| node_order = {node: i for i, node in enumerate(G)} |
| for x, y in G.edges(): |
| if x != y: |
| idx, jdx = (node_order[i] for i in (x, y)) |
| K[idx, jdx] = a |
|
|
| |
| p = np.asarray(list(pos.values())) |
|
|
| |
| rho = scaling * np.sqrt(N) |
|
|
| |
| error = etol + 1 |
| n_iter = 0 |
| while error > etol: |
| diff = p[:, np.newaxis] - p[np.newaxis] |
| A = np.linalg.norm(diff, axis=-1)[..., np.newaxis] |
| |
| |
| |
| with warnings.catch_warnings(): |
| warnings.simplefilter("ignore") |
| change = K[..., np.newaxis] * diff - rho / A * diff |
| change = np.nansum(change, axis=0) |
| p += change * dt |
|
|
| error = np.linalg.norm(change, axis=-1).sum() |
| if n_iter > max_iter: |
| break |
| n_iter += 1 |
|
|
| pos = dict(zip(G.nodes(), p)) |
|
|
| if store_pos_as is not None: |
| nx.set_node_attributes(G, pos, store_pos_as) |
|
|
| return pos |
|
|
|
|
| @np_random_state("seed") |
| @nx._dispatchable(edge_attrs="weight", mutates_input={"store_pos_as": 15}) |
| def forceatlas2_layout( |
| G, |
| pos=None, |
| *, |
| max_iter=100, |
| jitter_tolerance=1.0, |
| scaling_ratio=2.0, |
| gravity=1.0, |
| distributed_action=False, |
| strong_gravity=False, |
| node_mass=None, |
| node_size=None, |
| weight=None, |
| linlog=False, |
| seed=None, |
| dim=2, |
| store_pos_as=None, |
| ): |
| """Position nodes using the ForceAtlas2 force-directed layout algorithm. |
| |
| This function applies the ForceAtlas2 layout algorithm [1]_ to a NetworkX graph, |
| positioning the nodes in a way that visually represents the structure of the graph. |
| The algorithm uses physical simulation to minimize the energy of the system, |
| resulting in a more readable layout. |
| |
| Parameters |
| ---------- |
| G : nx.Graph |
| A NetworkX graph to be laid out. |
| pos : dict or None, optional |
| Initial positions of the nodes. If None, random initial positions are used. |
| max_iter : int (default: 100) |
| Number of iterations for the layout optimization. |
| jitter_tolerance : float (default: 1.0) |
| Controls the tolerance for adjusting the speed of layout generation. |
| scaling_ratio : float (default: 2.0) |
| Determines the scaling of attraction and repulsion forces. |
| gravity : float (default: 1.0) |
| Determines the amount of attraction on nodes to the center. Prevents islands |
| (i.e. weakly connected or disconnected parts of the graph) |
| from drifting away. |
| distributed_action : bool (default: False) |
| Distributes the attraction force evenly among nodes. |
| strong_gravity : bool (default: False) |
| Applies a strong gravitational pull towards the center. |
| node_mass : dict or None, optional |
| Maps nodes to their masses, influencing the attraction to other nodes. |
| node_size : dict or None, optional |
| Maps nodes to their sizes, preventing crowding by creating a halo effect. |
| weight : string or None, optional (default: None) |
| The edge attribute that holds the numerical value used for |
| the edge weight. If None, then all edge weights are 1. |
| linlog : bool (default: False) |
| Uses logarithmic attraction instead of linear. |
| seed : int, RandomState instance or None optional (default=None) |
| Used only for the initial positions in the algorithm. |
| Set the random state for deterministic node layouts. |
| If int, `seed` is the seed used by the random number generator, |
| if numpy.random.RandomState instance, `seed` is the random |
| number generator, |
| if None, the random number generator is the RandomState instance used |
| by numpy.random. |
| dim : int (default: 2) |
| Sets the dimensions for the layout. Ignored if `pos` is provided. |
| store_pos_as : str, default None |
| If non-None, the position of each node will be stored on the graph as |
| an attribute with this string as its name, which can be accessed with |
| ``G.nodes[...][store_pos_as]``. The function still returns the dictionary. |
| |
| Examples |
| -------- |
| >>> import networkx as nx |
| >>> G = nx.florentine_families_graph() |
| >>> pos = nx.forceatlas2_layout(G) |
| >>> nx.draw(G, pos=pos) |
| >>> # suppress the returned dict and store on the graph directly |
| >>> pos = nx.forceatlas2_layout(G, store_pos_as="pos") |
| >>> _ = nx.forceatlas2_layout(G, store_pos_as="pos") |
| |
| References |
| ---------- |
| .. [1] Jacomy, M., Venturini, T., Heymann, S., & Bastian, M. (2014). |
| ForceAtlas2, a continuous graph layout algorithm for handy network |
| visualization designed for the Gephi software. PloS one, 9(6), e98679. |
| https://doi.org/10.1371/journal.pone.0098679 |
| """ |
| import numpy as np |
|
|
| if len(G) == 0: |
| return {} |
| |
| if pos is None: |
| pos = nx.random_layout(G, dim=dim, seed=seed) |
| pos_arr = np.array(list(pos.values())) |
| elif len(pos) == len(G): |
| pos_arr = np.array([pos[node].copy() for node in G]) |
| else: |
| |
| pos_init = np.array(list(pos.values())) |
| max_pos = pos_init.max(axis=0) |
| min_pos = pos_init.min(axis=0) |
| dim = max_pos.size |
| pos_arr = min_pos + seed.rand(len(G), dim) * (max_pos - min_pos) |
| for idx, node in enumerate(G): |
| if node in pos: |
| pos_arr[idx] = pos[node].copy() |
|
|
| mass = np.zeros(len(G)) |
| size = np.zeros(len(G)) |
|
|
| |
| adjust_sizes = False |
| if node_size is None: |
| node_size = {} |
| else: |
| adjust_sizes = True |
|
|
| if node_mass is None: |
| node_mass = {} |
|
|
| for idx, node in enumerate(G): |
| mass[idx] = node_mass.get(node, G.degree(node) + 1) |
| size[idx] = node_size.get(node, 1) |
|
|
| n = len(G) |
| gravities = np.zeros((n, dim)) |
| attraction = np.zeros((n, dim)) |
| repulsion = np.zeros((n, dim)) |
| A = nx.to_numpy_array(G, weight=weight) |
|
|
| def estimate_factor(n, swing, traction, speed, speed_efficiency, jitter_tolerance): |
| """Computes the scaling factor for the force in the ForceAtlas2 layout algorithm. |
| |
| This helper function adjusts the speed and |
| efficiency of the layout generation based on the |
| current state of the system, such as the number of |
| nodes, current swing, and traction forces. |
| |
| Parameters |
| ---------- |
| n : int |
| Number of nodes in the graph. |
| swing : float |
| The current swing, representing the oscillation of the nodes. |
| traction : float |
| The current traction force, representing the attraction between nodes. |
| speed : float |
| The current speed of the layout generation. |
| speed_efficiency : float |
| The efficiency of the current speed, influencing how fast the layout converges. |
| jitter_tolerance : float |
| The tolerance for jitter, affecting how much speed adjustment is allowed. |
| |
| Returns |
| ------- |
| tuple |
| A tuple containing the updated speed and speed efficiency. |
| |
| Notes |
| ----- |
| This function is a part of the ForceAtlas2 layout algorithm and is used to dynamically adjust the |
| layout parameters to achieve an optimal and stable visualization. |
| |
| """ |
| import numpy as np |
|
|
| |
| opt_jitter = 0.05 * np.sqrt(n) |
| min_jitter = np.sqrt(opt_jitter) |
| max_jitter = 10 |
| min_speed_efficiency = 0.05 |
|
|
| other = min(max_jitter, opt_jitter * traction / n**2) |
| jitter = jitter_tolerance * max(min_jitter, other) |
|
|
| if swing / traction > 2.0: |
| if speed_efficiency > min_speed_efficiency: |
| speed_efficiency *= 0.5 |
| jitter = max(jitter, jitter_tolerance) |
| if swing == 0: |
| target_speed = np.inf |
| else: |
| target_speed = jitter * speed_efficiency * traction / swing |
|
|
| if swing > jitter * traction: |
| if speed_efficiency > min_speed_efficiency: |
| speed_efficiency *= 0.7 |
| elif speed < 1000: |
| speed_efficiency *= 1.3 |
|
|
| max_rise = 0.5 |
| speed = speed + min(target_speed - speed, max_rise * speed) |
| return speed, speed_efficiency |
|
|
| speed = 1 |
| speed_efficiency = 1 |
| swing = 1 |
| traction = 1 |
| for _ in range(max_iter): |
| |
| diff = pos_arr[:, None] - pos_arr[None] |
| |
| distance = np.linalg.norm(diff, axis=-1) |
|
|
| |
| if linlog: |
| attraction = -np.log(1 + distance) / distance |
| np.fill_diagonal(attraction, 0) |
| attraction = np.einsum("ij, ij -> ij", attraction, A) |
| attraction = np.einsum("ijk, ij -> ik", diff, attraction) |
|
|
| else: |
| attraction = -np.einsum("ijk, ij -> ik", diff, A) |
|
|
| if distributed_action: |
| attraction /= mass[:, None] |
|
|
| |
| tmp = mass[:, None] @ mass[None] |
| if adjust_sizes: |
| distance += -size[:, None] - size[None] |
|
|
| d2 = distance**2 |
| |
| np.fill_diagonal(tmp, 0) |
| np.fill_diagonal(d2, 1) |
| factor = (tmp / d2) * scaling_ratio |
| repulsion = np.einsum("ijk, ij -> ik", diff, factor) |
|
|
| |
| pos_centered = pos_arr - np.mean(pos_arr, axis=0) |
| if strong_gravity: |
| gravities = -gravity * mass[:, None] * pos_centered |
| else: |
| |
| with np.errstate(divide="ignore", invalid="ignore"): |
| unit_vec = pos_centered / np.linalg.norm(pos_centered, axis=-1)[:, None] |
| unit_vec = np.nan_to_num(unit_vec, nan=0) |
| gravities = -gravity * mass[:, None] * unit_vec |
|
|
| |
| update = attraction + repulsion + gravities |
|
|
| |
| swing += (mass * np.linalg.norm(pos_arr - update, axis=-1)).sum() |
| traction += (0.5 * mass * np.linalg.norm(pos_arr + update, axis=-1)).sum() |
|
|
| speed, speed_efficiency = estimate_factor( |
| n, |
| swing, |
| traction, |
| speed, |
| speed_efficiency, |
| jitter_tolerance, |
| ) |
|
|
| |
| if adjust_sizes: |
| df = np.linalg.norm(update, axis=-1) |
| swinging = mass * df |
| factor = 0.1 * speed / (1 + np.sqrt(speed * swinging)) |
| factor = np.minimum(factor * df, 10.0 * np.ones(df.shape)) / df |
| else: |
| swinging = mass * np.linalg.norm(update, axis=-1) |
| factor = speed / (1 + np.sqrt(speed * swinging)) |
|
|
| factored_update = update * factor[:, None] |
| pos_arr += factored_update |
| if abs(factored_update).sum() < 1e-10: |
| break |
|
|
| pos = dict(zip(G, pos_arr)) |
| if store_pos_as is not None: |
| nx.set_node_attributes(G, pos, store_pos_as) |
|
|
| return pos |
|
|
|
|
| def rescale_layout(pos, scale=1): |
| """Returns scaled position array to (-scale, scale) in all axes. |
| |
| The function acts on NumPy arrays which hold position information. |
| Each position is one row of the array. The dimension of the space |
| equals the number of columns. Each coordinate in one column. |
| |
| To rescale, the mean (center) is subtracted from each axis separately. |
| Then all values are scaled so that the largest magnitude value |
| from all axes equals `scale` (thus, the aspect ratio is preserved). |
| The resulting NumPy Array is returned (order of rows unchanged). |
| |
| Parameters |
| ---------- |
| pos : numpy array |
| positions to be scaled. Each row is a position. |
| |
| scale : number (default: 1) |
| The size of the resulting extent in all directions. |
| |
| attribute : str, default None |
| If non-None, the position of each node will be stored on the graph as |
| an attribute named `attribute` which can be accessed with |
| `G.nodes[...][attribute]`. The function still returns the dictionary. |
| |
| Returns |
| ------- |
| pos : numpy array |
| scaled positions. Each row is a position. |
| |
| See Also |
| -------- |
| rescale_layout_dict |
| """ |
| import numpy as np |
|
|
| |
| pos -= pos.mean(axis=0) |
| lim = np.abs(pos).max() |
| |
| if lim > 0: |
| pos *= scale / lim |
| return pos |
|
|
|
|
| def rescale_layout_dict(pos, scale=1): |
| """Return a dictionary of scaled positions keyed by node |
| |
| Parameters |
| ---------- |
| pos : A dictionary of positions keyed by node |
| |
| scale : number (default: 1) |
| The size of the resulting extent in all directions. |
| |
| Returns |
| ------- |
| pos : A dictionary of positions keyed by node |
| |
| Examples |
| -------- |
| >>> import numpy as np |
| >>> pos = {0: np.array((0, 0)), 1: np.array((1, 1)), 2: np.array((0.5, 0.5))} |
| >>> nx.rescale_layout_dict(pos) |
| {0: array([-1., -1.]), 1: array([1., 1.]), 2: array([0., 0.])} |
| |
| >>> pos = {0: np.array((0, 0)), 1: np.array((-1, 1)), 2: np.array((-0.5, 0.5))} |
| >>> nx.rescale_layout_dict(pos, scale=2) |
| {0: array([ 2., -2.]), 1: array([-2., 2.]), 2: array([0., 0.])} |
| |
| See Also |
| -------- |
| rescale_layout |
| """ |
| import numpy as np |
|
|
| if not pos: |
| return {} |
| pos_v = np.array(list(pos.values())) |
| pos_v = rescale_layout(pos_v, scale=scale) |
| return dict(zip(pos, pos_v)) |
|
|
|
|
| def bfs_layout(G, start, *, align="vertical", scale=1, center=None, store_pos_as=None): |
| """Position nodes according to breadth-first search algorithm. |
| |
| Parameters |
| ---------- |
| G : NetworkX graph |
| A position will be assigned to every node in G. |
| |
| start : node in `G` |
| Starting node for bfs |
| |
| align : string (default='vertical') |
| The alignment of nodes within a layer, either `"vertical"` or |
| `"horizontal"`. |
| |
| scale : number (default: 1) |
| Scale factor for positions. |
| |
| center : array-like or None |
| Coordinate pair around which to center the layout. |
| |
| store_pos_as : str, default None |
| If non-None, the position of each node will be stored on the graph as |
| an attribute with this string as its name, which can be accessed with |
| ``G.nodes[...][store_pos_as]``. The function still returns the dictionary. |
| |
| Returns |
| ------- |
| pos : dict |
| A dictionary of positions keyed by node. |
| |
| Examples |
| -------- |
| >>> from pprint import pprint |
| >>> G = nx.path_graph(4) |
| >>> pos = nx.bfs_layout(G, 0) |
| >>> # suppress the returned dict and store on the graph directly |
| >>> _ = nx.bfs_layout(G, 0, store_pos_as="pos") |
| >>> pprint(nx.get_node_attributes(G, "pos")) |
| {0: array([-1., 0.]), |
| 1: array([-0.33333333, 0. ]), |
| 2: array([0.33333333, 0. ]), |
| 3: array([1., 0.])} |
| |
| |
| |
| Notes |
| ----- |
| This algorithm currently only works in two dimensions and does not |
| try to minimize edge crossings. |
| |
| """ |
| G, center = _process_params(G, center, 2) |
|
|
| |
| layers = dict(enumerate(nx.bfs_layers(G, start))) |
|
|
| if len(G) != sum(len(nodes) for nodes in layers.values()): |
| raise nx.NetworkXError( |
| "bfs_layout didn't include all nodes. Perhaps use input graph:\n" |
| " G.subgraph(nx.node_connected_component(G, start))" |
| ) |
|
|
| |
| pos = multipartite_layout( |
| G, subset_key=layers, align=align, scale=scale, center=center |
| ) |
|
|
| if store_pos_as is not None: |
| nx.set_node_attributes(G, pos, store_pos_as) |
|
|
| return pos |
|
|