| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036 |
- """
- ******
- 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):
- # Some boilerplate code.
- 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):
- # dim=2 only
- """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:
- # Discard the extra angle since it matches 0 radians.
- 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:
- # draw the whole graph in one shell
- nlist = [list(G)]
- radius_bump = scale / len(nlist)
- if len(nlist[0]) == 1:
- # single node at center
- radius = 0.0
- else:
- # else start at r=1
- radius = radius_bump
- if rotate is None:
- rotate = np.pi / len(nlist)
- first_theta = rotate
- npos = {}
- for nodes in nlist:
- # Discard the last angle (endpoint=False) since 2*pi matches 0 radians
- 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
- # Preserves backward-compatible node ordering in returned pos dict
- 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] # swap x and y coords
- 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:
- # Determine size of existing domain to adjust initial positions
- 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
- # Sparse matrix
- 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:
- # We must adjust k by domain size for layouts not near 1x1
- 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:
- # We must adjust k by domain size for layouts not near 1x1
- 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
- ):
- # Position nodes in adjacency matrix A using Fruchterman-Reingold
- # Entry point for NetworkX graph is fruchterman_reingold_layout()
- 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:
- # random initial positions
- pos = np.asarray(seed.rand(nnodes, dim), dtype=A.dtype)
- else:
- # make sure positions are of same type as matrix
- pos = pos.astype(A.dtype)
- # optimal distance between nodes
- if k is None:
- k = np.sqrt(1.0 / nnodes)
- # the initial "temperature" is about .1 of domain area (=1x1)
- # this is the largest step allowed in the dynamics.
- # We need to calculate this in case our fixed positions force our domain
- # to be much bigger than 1x1
- t = max(max(pos.T[0]) - min(pos.T[0]), max(pos.T[1]) - min(pos.T[1])) * 0.1
- # simple cooling scheme.
- # linearly step down by dt on each iteration so last iteration is size dt.
- dt = t / (iterations + 1)
- delta = np.zeros((pos.shape[0], pos.shape[0], pos.shape[1]), dtype=A.dtype)
- # the inscrutable (but fast) version
- # this is still O(V^2)
- # could use multilevel methods to speed this up significantly
- for iteration in range(iterations):
- # matrix of difference between points
- delta = pos[:, np.newaxis, :] - pos[np.newaxis, :, :]
- # distance between points
- distance = np.linalg.norm(delta, axis=-1)
- # enforce minimum distance of 0.01
- np.clip(distance, 0.01, None, out=distance)
- # displacement "force"
- displacement = np.einsum(
- "ijk,ij->ik", delta, (k * k / distance**2 - A * distance / k)
- )
- # update positions
- length = np.linalg.norm(displacement, axis=-1)
- # Threshold the minimum length prior to position scaling
- # See gh-8113 for detailed discussion of the threshold
- 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:
- # don't change positions of fixed nodes
- delta_pos[fixed] = 0.0
- pos += delta_pos
- # cool temperature
- 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,
- ):
- # Position nodes in adjacency matrix A using Fruchterman-Reingold
- # Entry point for NetworkX graph is fruchterman_reingold_layout()
- # Sparse version
- 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:
- # random initial positions
- pos = np.asarray(seed.rand(nnodes, dim), dtype=A.dtype)
- else:
- # make sure positions are of same type as matrix
- pos = pos.astype(A.dtype)
- # no fixed nodes
- if fixed is None:
- fixed = []
- # optimal distance between nodes
- 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
- )
- # make sure we have a LIst of Lists representation
- try:
- A = A.tolil()
- except AttributeError:
- A = (sp.sparse.coo_array(A)).tolil()
- # the initial "temperature" is about .1 of domain area (=1x1)
- # this is the largest step allowed in the dynamics.
- t = max(max(pos.T[0]) - min(pos.T[0]), max(pos.T[1]) - min(pos.T[1])) * 0.1
- # simple cooling scheme.
- # linearly step down by dt on each iteration so last iteration is size dt.
- dt = t / (iterations + 1)
- displacement = np.zeros((dim, nnodes))
- for iteration in range(iterations):
- displacement *= 0
- # loop over rows
- for i in range(A.shape[0]):
- if i in fixed:
- continue
- # difference between this row's node position and all others
- delta = (pos[i] - pos).T
- # distance between points
- distance = np.sqrt((delta**2).sum(axis=0))
- # enforce minimum distance of 0.01
- distance = np.clip(distance, a_min=0.01, a_max=None)
- # the adjacency matrix row
- Ai = A.getrowview(i).toarray() # TODO: revisit w/ sparse 1D container
- # displacement "force"
- displacement[:, i] += (
- delta * (k * k / distance**2 - Ai * distance / k)
- ).sum(axis=1)
- # update positions
- length = np.sqrt((displacement**2).sum(axis=0))
- # Threshold the minimum length prior to position scaling
- # See gh-8113 for detailed discussion of the threshold
- length = np.clip(length, a_min=0.01, a_max=None)
- delta_pos = (displacement * t / length).T
- pos += delta_pos
- # cool temperature
- 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
- ):
- # Entry point for NetworkX graph is fruchterman_reingold_layout()
- # energy-based version
- import numpy as np
- import scipy as sp
- if gravity <= 0:
- raise ValueError(f"the gravity must be positive.")
- # make sure we have a Compressed Sparse Row format
- try:
- A = A.tocsr()
- except AttributeError:
- A = sp.sparse.csr_array(A)
- # Take absolute values of edge weights and symmetrize it
- 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)
- # difference between selected node positions and all others
- delta = pos[l:r, np.newaxis, :] - pos[np.newaxis, :, :]
- # distance between points with a minimum distance of 1e-5
- distance2 = np.sum(delta * delta, axis=2)
- distance2 = np.maximum(distance2, 1e-10)
- distance = np.sqrt(distance2)
- # temporary variable for calculation
- Ad = A[l:r] * distance
- # attractive forces and repulsive forces
- grad[l:r] = 2 * np.einsum("ij,ijk->ik", Ad / k - k**2 / distance2, delta)
- # integrated attractive forces
- cost += np.sum(Ad * distance2) / (3 * k)
- # integrated repulsive forces
- cost -= k**2 * np.sum(np.log(distance))
- # gravitational force from the centroids of connected components to (0.5, ..., 0.5)^T
- 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)
- # fix positions of fixed nodes
- grad[fixed] = 0.0
- return cost, grad.ravel()
- # Optimization of the energy function by L-BFGS algorithm
- 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):
- # Anneal node locations based on the Kamada-Kawai cost-function,
- # using the supplied matrix of preferred inter-node distances,
- # and starting locations.
- 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):
- # Cost-function and gradient for Kamada-Kawai layout algorithm
- 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
- )
- # Additional parabolic term to encourage mean position to be near origin:
- 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).
- """
- # handle some special cases that break the eigensolvers
- 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:
- # Sparse matrix
- if len(G) < 500: # dense solver is faster for small graphs
- raise ValueError
- A = nx.to_scipy_sparse_array(G, weight=weight, dtype="d")
- # Symmetrize directed graphs
- if G.is_directed():
- A = A + np.transpose(A)
- pos = _sparse_spectral(A, dim)
- except (ImportError, ValueError):
- # Dense matrix
- A = nx.to_numpy_array(G, weight=weight)
- # Symmetrize directed graphs
- 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):
- # Input adjacency matrix A
- # Uses dense eigenvalue solver from numpy
- 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
- # form Laplacian matrix where D is diagonal of degrees
- D = np.identity(nnodes, dtype=A.dtype) * np.sum(A, axis=1)
- L = D - A
- eigenvalues, eigenvectors = np.linalg.eig(L)
- # sort and keep smallest nonzero
- index = np.argsort(eigenvalues)[1 : dim + 1] # 0 index is zero eigenvalue
- return np.real(eigenvectors[:, index])
- def _sparse_spectral(A, dim=2):
- # Input adjacency matrix A
- # Uses sparse eigenvalue solver from scipy
- # Could use multilevel methods here, see Koren "On spectral graph drawing"
- 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
- # form Laplacian matrix
- D = sp.sparse.dia_array((A.sum(axis=1), 0), shape=(nnodes, nnodes)).tocsr()
- L = D - A
- k = dim + 1
- # number of Lanczos vectors for ARPACK solver.What is the right scaling?
- ncv = max(2 * k + 1, int(np.sqrt(nnodes)))
- # return smallest k eigenvalues and eigenvectors
- eigenvalues, eigenvectors = sp.sparse.linalg.eigsh(L, k, which="SM", ncv=ncv)
- index = np.argsort(eigenvalues)[1:k] # 0 index is zero eigenvalue
- 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:
- # check if subset_key is dict-like
- 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:
- # subset_key is not a dict, hence a string
- 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)
- # Sort by layer, if possible
- 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] # swap x and y coords
- 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()
- # Initialize spring constant matrix
- N = len(G)
- # No nodes no computation
- if N == 0:
- return pos
- # init force of springs
- 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
- # vectorize values
- p = np.asarray(list(pos.values()))
- # equation 10 in [1]
- rho = scaling * np.sqrt(N)
- # looping variables
- 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]
- # attraction_force - repulsions force
- # suppress nans due to division; caused by diagonal set to zero.
- # Does not affect the computation due to nansum
- 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 {}
- # parse optional pos positions
- 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:
- # set random node pos within the initial pos values
- 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))
- # Only adjust for size when the users specifies size other than default (1)
- 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
- # estimate jitter
- 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):
- # compute pairwise difference
- diff = pos_arr[:, None] - pos_arr[None]
- # compute pairwise distance
- distance = np.linalg.norm(diff, axis=-1)
- # linear attraction
- 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]
- # repulsion
- tmp = mass[:, None] @ mass[None]
- if adjust_sizes:
- distance += -size[:, None] - size[None]
- d2 = distance**2
- # remove self-interaction
- np.fill_diagonal(tmp, 0)
- np.fill_diagonal(d2, 1)
- factor = (tmp / d2) * scaling_ratio
- repulsion = np.einsum("ijk, ij -> ik", diff, factor)
- # gravity
- pos_centered = pos_arr - np.mean(pos_arr, axis=0)
- if strong_gravity:
- gravities = -gravity * mass[:, None] * pos_centered
- else:
- # hide warnings for divide by zero. Then change nan to 0
- 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
- # total forces
- update = attraction + repulsion + gravities
- # compute total swing and traction
- 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,
- )
- # update pos
- 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
- # Find max length over all dimensions
- pos -= pos.mean(axis=0)
- lim = np.abs(pos).max() # max coordinate for all axes
- # rescale to (-scale, scale) in all directions, preserves aspect
- 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: # empty_graph
- 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)
- # Compute layers with BFS
- 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))"
- )
- # Compute node positions with multipartite_layout
- 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
|