comparison env/lib/python3.7/site-packages/networkx/drawing/layout.py @ 0:26e78fe6e8c4 draft

"planemo upload commit c699937486c35866861690329de38ec1a5d9f783"
author shellac
date Sat, 02 May 2020 07:14:21 -0400
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:26e78fe6e8c4
1 # Copyright (C) 2004-2019 by
2 # Aric Hagberg <hagberg@lanl.gov>
3 # Dan Schult <dschult@colgate.edu>
4 # Pieter Swart <swart@lanl.gov>
5 # Richard Penney <rwpenney@users.sourceforge.net>
6 # Michael Fedell <mfedell@jpl.nasa.gov>
7 # Valentino Constantinou <vconstan@jpl.nasa.gov>
8 # All rights reserved.
9 # BSD license.
10 #
11 # Authors: Aric Hagberg <aric.hagberg@gmail.com>,
12 # Dan Schult <dschult@colgate.edu>
13 """
14 ******
15 Layout
16 ******
17
18 Node positioning algorithms for graph drawing.
19
20 For `random_layout()` the possible resulting shape
21 is a square of side [0, scale] (default: [0, 1])
22 Changing `center` shifts the layout by that amount.
23
24 For the other layout routines, the extent is
25 [center - scale, center + scale] (default: [-1, 1]).
26
27 Warning: Most layout routines have only been tested in 2-dimensions.
28
29 """
30 import networkx as nx
31 from networkx.utils import random_state
32
33 __all__ = ['bipartite_layout',
34 'circular_layout',
35 'kamada_kawai_layout',
36 'random_layout',
37 'rescale_layout',
38 'shell_layout',
39 'spring_layout',
40 'spectral_layout',
41 'planar_layout',
42 'fruchterman_reingold_layout',
43 'spiral_layout']
44
45
46 def _process_params(G, center, dim):
47 # Some boilerplate code.
48 import numpy as np
49
50 if not isinstance(G, nx.Graph):
51 empty_graph = nx.Graph()
52 empty_graph.add_nodes_from(G)
53 G = empty_graph
54
55 if center is None:
56 center = np.zeros(dim)
57 else:
58 center = np.asarray(center)
59
60 if len(center) != dim:
61 msg = "length of center coordinates must match dimension of layout"
62 raise ValueError(msg)
63
64 return G, center
65
66
67 @random_state(3)
68 def random_layout(G, center=None, dim=2, seed=None):
69 """Position nodes uniformly at random in the unit square.
70
71 For every node, a position is generated by choosing each of dim
72 coordinates uniformly at random on the interval [0.0, 1.0).
73
74 NumPy (http://scipy.org) is required for this function.
75
76 Parameters
77 ----------
78 G : NetworkX graph or list of nodes
79 A position will be assigned to every node in G.
80
81 center : array-like or None
82 Coordinate pair around which to center the layout.
83
84 dim : int
85 Dimension of layout.
86
87 seed : int, RandomState instance or None optional (default=None)
88 Set the random state for deterministic node layouts.
89 If int, `seed` is the seed used by the random number generator,
90 if numpy.random.RandomState instance, `seed` is the random
91 number generator,
92 if None, the random number generator is the RandomState instance used
93 by numpy.random.
94
95 Returns
96 -------
97 pos : dict
98 A dictionary of positions keyed by node
99
100 Examples
101 --------
102 >>> G = nx.lollipop_graph(4, 3)
103 >>> pos = nx.random_layout(G)
104
105 """
106 import numpy as np
107
108 G, center = _process_params(G, center, dim)
109 pos = seed.rand(len(G), dim) + center
110 pos = pos.astype(np.float32)
111 pos = dict(zip(G, pos))
112
113 return pos
114
115
116 def circular_layout(G, scale=1, center=None, dim=2):
117 # dim=2 only
118 """Position nodes on a circle.
119
120 Parameters
121 ----------
122 G : NetworkX graph or list of nodes
123 A position will be assigned to every node in G.
124
125 scale : number (default: 1)
126 Scale factor for positions.
127
128 center : array-like or None
129 Coordinate pair around which to center the layout.
130
131 dim : int
132 Dimension of layout.
133 If dim>2, the remaining dimensions are set to zero
134 in the returned positions.
135 If dim<2, a ValueError is raised.
136
137 Returns
138 -------
139 pos : dict
140 A dictionary of positions keyed by node
141
142 Raises
143 -------
144 ValueError
145 If dim < 2
146
147 Examples
148 --------
149 >>> G = nx.path_graph(4)
150 >>> pos = nx.circular_layout(G)
151
152 Notes
153 -----
154 This algorithm currently only works in two dimensions and does not
155 try to minimize edge crossings.
156
157 """
158 import numpy as np
159
160 if dim < 2:
161 raise ValueError('cannot handle dimensions < 2')
162
163 G, center = _process_params(G, center, dim)
164
165 paddims = max(0, (dim - 2))
166
167 if len(G) == 0:
168 pos = {}
169 elif len(G) == 1:
170 pos = {nx.utils.arbitrary_element(G): center}
171 else:
172 # Discard the extra angle since it matches 0 radians.
173 theta = np.linspace(0, 1, len(G) + 1)[:-1] * 2 * np.pi
174 theta = theta.astype(np.float32)
175 pos = np.column_stack([np.cos(theta), np.sin(theta),
176 np.zeros((len(G), paddims))])
177 pos = rescale_layout(pos, scale=scale) + center
178 pos = dict(zip(G, pos))
179
180 return pos
181
182
183 def shell_layout(G, nlist=None, scale=1, center=None, dim=2):
184 """Position nodes in concentric circles.
185
186 Parameters
187 ----------
188 G : NetworkX graph or list of nodes
189 A position will be assigned to every node in G.
190
191 nlist : list of lists
192 List of node lists for each shell.
193
194 scale : number (default: 1)
195 Scale factor for positions.
196
197 center : array-like or None
198 Coordinate pair around which to center the layout.
199
200 dim : int
201 Dimension of layout, currently only dim=2 is supported.
202 Other dimension values result in a ValueError.
203
204 Returns
205 -------
206 pos : dict
207 A dictionary of positions keyed by node
208
209 Raises
210 -------
211 ValueError
212 If dim != 2
213
214 Examples
215 --------
216 >>> G = nx.path_graph(4)
217 >>> shells = [[0], [1, 2, 3]]
218 >>> pos = nx.shell_layout(G, shells)
219
220 Notes
221 -----
222 This algorithm currently only works in two dimensions and does not
223 try to minimize edge crossings.
224
225 """
226 import numpy as np
227
228 if dim != 2:
229 raise ValueError('can only handle 2 dimensions')
230
231 G, center = _process_params(G, center, dim)
232
233 if len(G) == 0:
234 return {}
235 if len(G) == 1:
236 return {nx.utils.arbitrary_element(G): center}
237
238 if nlist is None:
239 # draw the whole graph in one shell
240 nlist = [list(G)]
241
242 if len(nlist[0]) == 1:
243 # single node at center
244 radius = 0.0
245 else:
246 # else start at r=1
247 radius = 1.0
248
249 npos = {}
250 for nodes in nlist:
251 # Discard the extra angle since it matches 0 radians.
252 theta = np.linspace(0, 1, len(nodes) + 1)[:-1] * 2 * np.pi
253 theta = theta.astype(np.float32)
254 pos = np.column_stack([np.cos(theta), np.sin(theta)])
255 if len(pos) > 1:
256 pos = rescale_layout(pos, scale=scale * radius / len(nlist)) + center
257 else:
258 pos = np.array([(scale * radius + center[0], center[1])])
259 npos.update(zip(nodes, pos))
260 radius += 1.0
261
262 return npos
263
264
265 def bipartite_layout(G, nodes, align='vertical',
266 scale=1, center=None, aspect_ratio=4/3):
267 """Position nodes in two straight lines.
268
269 Parameters
270 ----------
271 G : NetworkX graph or list of nodes
272 A position will be assigned to every node in G.
273
274 nodes : list or container
275 Nodes in one node set of the bipartite graph.
276 This set will be placed on left or top.
277
278 align : string (default='vertical')
279 The alignment of nodes. Vertical or horizontal.
280
281 scale : number (default: 1)
282 Scale factor for positions.
283
284 center : array-like or None
285 Coordinate pair around which to center the layout.
286
287 aspect_ratio : number (default=4/3):
288 The ratio of the width to the height of the layout.
289
290 Returns
291 -------
292 pos : dict
293 A dictionary of positions keyed by node.
294
295 Examples
296 --------
297 >>> G = nx.bipartite.gnmk_random_graph(3, 5, 10, seed=123)
298 >>> top = nx.bipartite.sets(G)[0]
299 >>> pos = nx.bipartite_layout(G, top)
300
301 Notes
302 -----
303 This algorithm currently only works in two dimensions and does not
304 try to minimize edge crossings.
305
306 """
307
308 import numpy as np
309
310 G, center = _process_params(G, center=center, dim=2)
311 if len(G) == 0:
312 return {}
313
314 height = 1
315 width = aspect_ratio * height
316 offset = (width/2, height/2)
317
318 top = set(nodes)
319 bottom = set(G) - top
320 nodes = list(top) + list(bottom)
321
322 if align == 'vertical':
323 left_xs = np.repeat(0, len(top))
324 right_xs = np.repeat(width, len(bottom))
325 left_ys = np.linspace(0, height, len(top))
326 right_ys = np.linspace(0, height, len(bottom))
327
328 top_pos = np.column_stack([left_xs, left_ys]) - offset
329 bottom_pos = np.column_stack([right_xs, right_ys]) - offset
330
331 pos = np.concatenate([top_pos, bottom_pos])
332 pos = rescale_layout(pos, scale=scale) + center
333 pos = dict(zip(nodes, pos))
334 return pos
335
336 if align == 'horizontal':
337 top_ys = np.repeat(height, len(top))
338 bottom_ys = np.repeat(0, len(bottom))
339 top_xs = np.linspace(0, width, len(top))
340 bottom_xs = np.linspace(0, width, len(bottom))
341
342 top_pos = np.column_stack([top_xs, top_ys]) - offset
343 bottom_pos = np.column_stack([bottom_xs, bottom_ys]) - offset
344
345 pos = np.concatenate([top_pos, bottom_pos])
346 pos = rescale_layout(pos, scale=scale) + center
347 pos = dict(zip(nodes, pos))
348 return pos
349
350 msg = 'align must be either vertical or horizontal.'
351 raise ValueError(msg)
352
353
354 @random_state(10)
355 def fruchterman_reingold_layout(G,
356 k=None,
357 pos=None,
358 fixed=None,
359 iterations=50,
360 threshold=1e-4,
361 weight='weight',
362 scale=1,
363 center=None,
364 dim=2,
365 seed=None):
366 """Position nodes using Fruchterman-Reingold force-directed algorithm.
367
368 The algorithm simulates a force-directed representation of the network
369 treating edges as springs holding nodes close, while treating nodes
370 as repelling objects, sometimes called an anti-gravity force.
371 Simulation continues until the positions are close to an equilibrium.
372
373 There are some hard-coded values: minimal distance between
374 nodes (0.01) and "temperature" of 0.1 to ensure nodes don't fly away.
375 During the simulation, `k` helps determine the distance between nodes,
376 though `scale` and `center` determine the size and place after
377 rescaling occurs at the end of the simulation.
378
379 Fixing some nodes doesn't allow them to move in the simulation.
380 It also turns off the rescaling feature at the simulation's end.
381 In addition, setting `scale` to `None` turns off rescaling.
382
383 Parameters
384 ----------
385 G : NetworkX graph or list of nodes
386 A position will be assigned to every node in G.
387
388 k : float (default=None)
389 Optimal distance between nodes. If None the distance is set to
390 1/sqrt(n) where n is the number of nodes. Increase this value
391 to move nodes farther apart.
392
393 pos : dict or None optional (default=None)
394 Initial positions for nodes as a dictionary with node as keys
395 and values as a coordinate list or tuple. If None, then use
396 random initial positions.
397
398 fixed : list or None optional (default=None)
399 Nodes to keep fixed at initial position.
400 ValueError raised if `fixed` specified and `pos` not.
401
402 iterations : int optional (default=50)
403 Maximum number of iterations taken
404
405 threshold: float optional (default = 1e-4)
406 Threshold for relative error in node position changes.
407 The iteration stops if the error is below this threshold.
408
409 weight : string or None optional (default='weight')
410 The edge attribute that holds the numerical value used for
411 the edge weight. If None, then all edge weights are 1.
412
413 scale : number or None (default: 1)
414 Scale factor for positions. Not used unless `fixed is None`.
415 If scale is None, no rescaling is performed.
416
417 center : array-like or None
418 Coordinate pair around which to center the layout.
419 Not used unless `fixed is None`.
420
421 dim : int
422 Dimension of layout.
423
424 seed : int, RandomState instance or None optional (default=None)
425 Set the random state for deterministic node layouts.
426 If int, `seed` is the seed used by the random number generator,
427 if numpy.random.RandomState instance, `seed` is the random
428 number generator,
429 if None, the random number generator is the RandomState instance used
430 by numpy.random.
431
432 Returns
433 -------
434 pos : dict
435 A dictionary of positions keyed by node
436
437 Examples
438 --------
439 >>> G = nx.path_graph(4)
440 >>> pos = nx.spring_layout(G)
441
442 # The same using longer but equivalent function name
443 >>> pos = nx.fruchterman_reingold_layout(G)
444 """
445 import numpy as np
446
447 G, center = _process_params(G, center, dim)
448
449 if fixed is not None:
450 if pos is None:
451 raise ValueError('nodes are fixed without positions given')
452 for node in fixed:
453 if node not in pos:
454 raise ValueError('nodes are fixed without positions given')
455 nfixed = {node: i for i, node in enumerate(G)}
456 fixed = np.asarray([nfixed[node] for node in fixed])
457
458 if pos is not None:
459 # Determine size of existing domain to adjust initial positions
460 dom_size = max(coord for pos_tup in pos.values() for coord in pos_tup)
461 if dom_size == 0:
462 dom_size = 1
463 pos_arr = seed.rand(len(G), dim) * dom_size + center
464
465 for i, n in enumerate(G):
466 if n in pos:
467 pos_arr[i] = np.asarray(pos[n])
468 else:
469 pos_arr = None
470 dom_size = 1
471
472 if len(G) == 0:
473 return {}
474 if len(G) == 1:
475 return {nx.utils.arbitrary_element(G.nodes()): center}
476
477 try:
478 # Sparse matrix
479 if len(G) < 500: # sparse solver for large graphs
480 raise ValueError
481 A = nx.to_scipy_sparse_matrix(G, weight=weight, dtype='f')
482 if k is None and fixed is not None:
483 # We must adjust k by domain size for layouts not near 1x1
484 nnodes, _ = A.shape
485 k = dom_size / np.sqrt(nnodes)
486 pos = _sparse_fruchterman_reingold(A, k, pos_arr, fixed,
487 iterations, threshold,
488 dim, seed)
489 except:
490 A = nx.to_numpy_array(G, weight=weight)
491 if k is None and fixed is not None:
492 # We must adjust k by domain size for layouts not near 1x1
493 nnodes, _ = A.shape
494 k = dom_size / np.sqrt(nnodes)
495 pos = _fruchterman_reingold(A, k, pos_arr, fixed, iterations,
496 threshold, dim, seed)
497 if fixed is None and scale is not None:
498 pos = rescale_layout(pos, scale=scale) + center
499 pos = dict(zip(G, pos))
500 return pos
501
502
503 spring_layout = fruchterman_reingold_layout
504
505
506 @random_state(7)
507 def _fruchterman_reingold(A, k=None, pos=None, fixed=None, iterations=50,
508 threshold=1e-4, dim=2, seed=None):
509 # Position nodes in adjacency matrix A using Fruchterman-Reingold
510 # Entry point for NetworkX graph is fruchterman_reingold_layout()
511 import numpy as np
512
513 try:
514 nnodes, _ = A.shape
515 except AttributeError:
516 msg = "fruchterman_reingold() takes an adjacency matrix as input"
517 raise nx.NetworkXError(msg)
518
519 if pos is None:
520 # random initial positions
521 pos = np.asarray(seed.rand(nnodes, dim), dtype=A.dtype)
522 else:
523 # make sure positions are of same type as matrix
524 pos = pos.astype(A.dtype)
525
526 # optimal distance between nodes
527 if k is None:
528 k = np.sqrt(1.0 / nnodes)
529 # the initial "temperature" is about .1 of domain area (=1x1)
530 # this is the largest step allowed in the dynamics.
531 # We need to calculate this in case our fixed positions force our domain
532 # to be much bigger than 1x1
533 t = max(max(pos.T[0]) - min(pos.T[0]), max(pos.T[1]) - min(pos.T[1])) * 0.1
534 # simple cooling scheme.
535 # linearly step down by dt on each iteration so last iteration is size dt.
536 dt = t / float(iterations + 1)
537 delta = np.zeros((pos.shape[0], pos.shape[0], pos.shape[1]), dtype=A.dtype)
538 # the inscrutable (but fast) version
539 # this is still O(V^2)
540 # could use multilevel methods to speed this up significantly
541 for iteration in range(iterations):
542 # matrix of difference between points
543 delta = pos[:, np.newaxis, :] - pos[np.newaxis, :, :]
544 # distance between points
545 distance = np.linalg.norm(delta, axis=-1)
546 # enforce minimum distance of 0.01
547 np.clip(distance, 0.01, None, out=distance)
548 # displacement "force"
549 displacement = np.einsum('ijk,ij->ik',
550 delta,
551 (k * k / distance**2 - A * distance / k))
552 # update positions
553 length = np.linalg.norm(displacement, axis=-1)
554 length = np.where(length < 0.01, 0.1, length)
555 delta_pos = np.einsum('ij,i->ij', displacement, t / length)
556 if fixed is not None:
557 # don't change positions of fixed nodes
558 delta_pos[fixed] = 0.0
559 pos += delta_pos
560 # cool temperature
561 t -= dt
562 err = np.linalg.norm(delta_pos) / nnodes
563 if err < threshold:
564 break
565 return pos
566
567
568 @random_state(7)
569 def _sparse_fruchterman_reingold(A, k=None, pos=None, fixed=None,
570 iterations=50, threshold=1e-4, dim=2,
571 seed=None):
572 # Position nodes in adjacency matrix A using Fruchterman-Reingold
573 # Entry point for NetworkX graph is fruchterman_reingold_layout()
574 # Sparse version
575 import numpy as np
576
577 try:
578 nnodes, _ = A.shape
579 except AttributeError:
580 msg = "fruchterman_reingold() takes an adjacency matrix as input"
581 raise nx.NetworkXError(msg)
582 try:
583 from scipy.sparse import spdiags, coo_matrix
584 except ImportError:
585 msg = "_sparse_fruchterman_reingold() scipy numpy: http://scipy.org/ "
586 raise ImportError(msg)
587 # make sure we have a LIst of Lists representation
588 try:
589 A = A.tolil()
590 except:
591 A = (coo_matrix(A)).tolil()
592
593 if pos is None:
594 # random initial positions
595 pos = np.asarray(seed.rand(nnodes, dim), dtype=A.dtype)
596 else:
597 # make sure positions are of same type as matrix
598 pos = pos.astype(A.dtype)
599
600 # no fixed nodes
601 if fixed is None:
602 fixed = []
603
604 # optimal distance between nodes
605 if k is None:
606 k = np.sqrt(1.0 / nnodes)
607 # the initial "temperature" is about .1 of domain area (=1x1)
608 # this is the largest step allowed in the dynamics.
609 t = max(max(pos.T[0]) - min(pos.T[0]), max(pos.T[1]) - min(pos.T[1])) * 0.1
610 # simple cooling scheme.
611 # linearly step down by dt on each iteration so last iteration is size dt.
612 dt = t / float(iterations + 1)
613
614 displacement = np.zeros((dim, nnodes))
615 for iteration in range(iterations):
616 displacement *= 0
617 # loop over rows
618 for i in range(A.shape[0]):
619 if i in fixed:
620 continue
621 # difference between this row's node position and all others
622 delta = (pos[i] - pos).T
623 # distance between points
624 distance = np.sqrt((delta**2).sum(axis=0))
625 # enforce minimum distance of 0.01
626 distance = np.where(distance < 0.01, 0.01, distance)
627 # the adjacency matrix row
628 Ai = np.asarray(A.getrowview(i).toarray())
629 # displacement "force"
630 displacement[:, i] +=\
631 (delta * (k * k / distance**2 - Ai * distance / k)).sum(axis=1)
632 # update positions
633 length = np.sqrt((displacement**2).sum(axis=0))
634 length = np.where(length < 0.01, 0.1, length)
635 delta_pos = (displacement * t / length).T
636 pos += delta_pos
637 # cool temperature
638 t -= dt
639 err = np.linalg.norm(delta_pos) / nnodes
640 if err < threshold:
641 break
642 return pos
643
644
645 def kamada_kawai_layout(G, dist=None,
646 pos=None,
647 weight='weight',
648 scale=1,
649 center=None,
650 dim=2):
651 """Position nodes using Kamada-Kawai path-length cost-function.
652
653 Parameters
654 ----------
655 G : NetworkX graph or list of nodes
656 A position will be assigned to every node in G.
657
658 dist : float (default=None)
659 A two-level dictionary of optimal distances between nodes,
660 indexed by source and destination node.
661 If None, the distance is computed using shortest_path_length().
662
663 pos : dict or None optional (default=None)
664 Initial positions for nodes as a dictionary with node as keys
665 and values as a coordinate list or tuple. If None, then use
666 circular_layout() for dim >= 2 and a linear layout for dim == 1.
667
668 weight : string or None optional (default='weight')
669 The edge attribute that holds the numerical value used for
670 the edge weight. If None, then all edge weights are 1.
671
672 scale : number (default: 1)
673 Scale factor for positions.
674
675 center : array-like or None
676 Coordinate pair around which to center the layout.
677
678 dim : int
679 Dimension of layout.
680
681 Returns
682 -------
683 pos : dict
684 A dictionary of positions keyed by node
685
686 Examples
687 --------
688 >>> G = nx.path_graph(4)
689 >>> pos = nx.kamada_kawai_layout(G)
690 """
691 import numpy as np
692
693 G, center = _process_params(G, center, dim)
694 nNodes = len(G)
695
696 if dist is None:
697 dist = dict(nx.shortest_path_length(G, weight=weight))
698 dist_mtx = 1e6 * np.ones((nNodes, nNodes))
699 for row, nr in enumerate(G):
700 if nr not in dist:
701 continue
702 rdist = dist[nr]
703 for col, nc in enumerate(G):
704 if nc not in rdist:
705 continue
706 dist_mtx[row][col] = rdist[nc]
707
708 if pos is None:
709 if dim >= 2:
710 pos = circular_layout(G, dim=dim)
711 else:
712 pos = {n: pt for n, pt in zip(G, np.linspace(0, 1, len(G)))}
713 pos_arr = np.array([pos[n] for n in G])
714
715 pos = _kamada_kawai_solve(dist_mtx, pos_arr, dim)
716
717 pos = rescale_layout(pos, scale=scale) + center
718 return dict(zip(G, pos))
719
720
721 def _kamada_kawai_solve(dist_mtx, pos_arr, dim):
722 # Anneal node locations based on the Kamada-Kawai cost-function,
723 # using the supplied matrix of preferred inter-node distances,
724 # and starting locations.
725
726 import numpy as np
727 from scipy.optimize import minimize
728
729 meanwt = 1e-3
730 costargs = (np, 1 / (dist_mtx + np.eye(dist_mtx.shape[0]) * 1e-3),
731 meanwt, dim)
732
733 optresult = minimize(_kamada_kawai_costfn, pos_arr.ravel(),
734 method='L-BFGS-B', args=costargs, jac=True)
735
736 return optresult.x.reshape((-1, dim))
737
738
739 def _kamada_kawai_costfn(pos_vec, np, invdist, meanweight, dim):
740 # Cost-function and gradient for Kamada-Kawai layout algorithm
741 nNodes = invdist.shape[0]
742 pos_arr = pos_vec.reshape((nNodes, dim))
743
744 delta = pos_arr[:, np.newaxis, :] - pos_arr[np.newaxis, :, :]
745 nodesep = np.linalg.norm(delta, axis=-1)
746 direction = np.einsum('ijk,ij->ijk',
747 delta,
748 1 / (nodesep + np.eye(nNodes) * 1e-3))
749
750 offset = nodesep * invdist - 1.0
751 offset[np.diag_indices(nNodes)] = 0
752
753 cost = 0.5 * np.sum(offset ** 2)
754 grad = (np.einsum('ij,ij,ijk->ik', invdist, offset, direction) -
755 np.einsum('ij,ij,ijk->jk', invdist, offset, direction))
756
757 # Additional parabolic term to encourage mean position to be near origin:
758 sumpos = np.sum(pos_arr, axis=0)
759 cost += 0.5 * meanweight * np.sum(sumpos ** 2)
760 grad += meanweight * sumpos
761
762 return (cost, grad.ravel())
763
764
765 def spectral_layout(G, weight='weight', scale=1, center=None, dim=2):
766 """Position nodes using the eigenvectors of the graph Laplacian.
767
768 Using the unnormalized Laplacion, the layout shows possible clusters of
769 nodes which are an approximation of the ratio cut. If dim is the number of
770 dimensions then the positions are the entries of the dim eigenvectors
771 corresponding to the ascending eigenvalues starting from the second one.
772
773 Parameters
774 ----------
775 G : NetworkX graph or list of nodes
776 A position will be assigned to every node in G.
777
778 weight : string or None optional (default='weight')
779 The edge attribute that holds the numerical value used for
780 the edge weight. If None, then all edge weights are 1.
781
782 scale : number (default: 1)
783 Scale factor for positions.
784
785 center : array-like or None
786 Coordinate pair around which to center the layout.
787
788 dim : int
789 Dimension of layout.
790
791 Returns
792 -------
793 pos : dict
794 A dictionary of positions keyed by node
795
796 Examples
797 --------
798 >>> G = nx.path_graph(4)
799 >>> pos = nx.spectral_layout(G)
800
801 Notes
802 -----
803 Directed graphs will be considered as undirected graphs when
804 positioning the nodes.
805
806 For larger graphs (>500 nodes) this will use the SciPy sparse
807 eigenvalue solver (ARPACK).
808 """
809 # handle some special cases that break the eigensolvers
810 import numpy as np
811
812 G, center = _process_params(G, center, dim)
813
814 if len(G) <= 2:
815 if len(G) == 0:
816 pos = np.array([])
817 elif len(G) == 1:
818 pos = np.array([center])
819 else:
820 pos = np.array([np.zeros(dim), np.array(center) * 2.0])
821 return dict(zip(G, pos))
822 try:
823 # Sparse matrix
824 if len(G) < 500: # dense solver is faster for small graphs
825 raise ValueError
826 A = nx.to_scipy_sparse_matrix(G, weight=weight, dtype='d')
827 # Symmetrize directed graphs
828 if G.is_directed():
829 A = A + np.transpose(A)
830 pos = _sparse_spectral(A, dim)
831 except (ImportError, ValueError):
832 # Dense matrix
833 A = nx.to_numpy_array(G, weight=weight)
834 # Symmetrize directed graphs
835 if G.is_directed():
836 A += A.T
837 pos = _spectral(A, dim)
838
839 pos = rescale_layout(pos, scale=scale) + center
840 pos = dict(zip(G, pos))
841 return pos
842
843
844 def _spectral(A, dim=2):
845 # Input adjacency matrix A
846 # Uses dense eigenvalue solver from numpy
847 import numpy as np
848
849 try:
850 nnodes, _ = A.shape
851 except AttributeError:
852 msg = "spectral() takes an adjacency matrix as input"
853 raise nx.NetworkXError(msg)
854
855 # form Laplacian matrix where D is diagonal of degrees
856 D = np.identity(nnodes, dtype=A.dtype) * np.sum(A, axis=1)
857 L = D - A
858
859 eigenvalues, eigenvectors = np.linalg.eig(L)
860 # sort and keep smallest nonzero
861 index = np.argsort(eigenvalues)[1:dim + 1] # 0 index is zero eigenvalue
862 return np.real(eigenvectors[:, index])
863
864
865 def _sparse_spectral(A, dim=2):
866 # Input adjacency matrix A
867 # Uses sparse eigenvalue solver from scipy
868 # Could use multilevel methods here, see Koren "On spectral graph drawing"
869 import numpy as np
870 from scipy.sparse import spdiags
871 from scipy.sparse.linalg.eigen import eigsh
872
873 try:
874 nnodes, _ = A.shape
875 except AttributeError:
876 msg = "sparse_spectral() takes an adjacency matrix as input"
877 raise nx.NetworkXError(msg)
878
879 # form Laplacian matrix
880 data = np.asarray(A.sum(axis=1).T)
881 D = spdiags(data, 0, nnodes, nnodes)
882 L = D - A
883
884 k = dim + 1
885 # number of Lanczos vectors for ARPACK solver.What is the right scaling?
886 ncv = max(2 * k + 1, int(np.sqrt(nnodes)))
887 # return smallest k eigenvalues and eigenvectors
888 eigenvalues, eigenvectors = eigsh(L, k, which='SM', ncv=ncv)
889 index = np.argsort(eigenvalues)[1:k] # 0 index is zero eigenvalue
890 return np.real(eigenvectors[:, index])
891
892
893 def planar_layout(G, scale=1, center=None, dim=2):
894 """Position nodes without edge intersections.
895
896 Parameters
897 ----------
898 G : NetworkX graph or list of nodes
899 A position will be assigned to every node in G. If G is of type
900 PlanarEmbedding, the positions are selected accordingly.
901
902 Parameters
903 ----------
904 G : NetworkX graph or list of nodes
905 A position will be assigned to every node in G. If G is of type
906 nx.PlanarEmbedding, the positions are selected accordingly.
907
908 scale : number (default: 1)
909 Scale factor for positions.
910
911 center : array-like or None
912 Coordinate pair around which to center the layout.
913
914 dim : int
915 Dimension of layout.
916
917 Returns
918 -------
919 pos : dict
920 A dictionary of positions keyed by node
921
922 Raises
923 ------
924 nx.NetworkXException
925 If G is not planar
926
927 Examples
928 --------
929 >>> G = nx.path_graph(4)
930 >>> pos = nx.planar_layout(G)
931 """
932 import numpy as np
933
934 if dim != 2:
935 raise ValueError('can only handle 2 dimensions')
936
937 G, center = _process_params(G, center, dim)
938
939 if len(G) == 0:
940 return {}
941
942 if isinstance(G, nx.PlanarEmbedding):
943 embedding = G
944 else:
945 is_planar, embedding = nx.check_planarity(G)
946 if not is_planar:
947 raise nx.NetworkXException("G is not planar.")
948 pos = nx.combinatorial_embedding_to_pos(embedding)
949 node_list = list(embedding)
950 pos = np.row_stack((pos[x] for x in node_list))
951 pos = pos.astype(np.float64)
952 pos = rescale_layout(pos, scale=scale) + center
953 return dict(zip(node_list, pos))
954
955
956 def spiral_layout(G, scale=1, center=None, dim=2,
957 resolution=0.35, equidistant=False):
958 """Position nodes in a spiral layout.
959
960 Parameters
961 ----------
962 G : NetworkX graph or list of nodes
963 A position will be assigned to every node in G.
964 scale : number (default: 1)
965 Scale factor for positions.
966 center : array-like or None
967 Coordinate pair around which to center the layout.
968 dim : int
969 Dimension of layout, currently only dim=2 is supported.
970 Other dimension values result in a ValueError.
971 resolution : float
972 The compactness of the spiral layout returned.
973 Lower values result in more compressed spiral layouts.
974 equidistant : bool
975 If True, nodes will be plotted equidistant from each other.
976 Returns
977 -------
978 pos : dict
979 A dictionary of positions keyed by node
980 Raises
981 -------
982 ValueError
983 If dim != 2
984 Examples
985 --------
986 >>> G = nx.path_graph(4)
987 >>> pos = nx.spiral_layout(G)
988
989 Notes
990 -----
991 This algorithm currently only works in two dimensions.
992
993 """
994 import numpy as np
995
996 if dim != 2:
997 raise ValueError('can only handle 2 dimensions')
998
999 G, center = _process_params(G, center, dim)
1000
1001 if len(G) == 0:
1002 return {}
1003 if len(G) == 1:
1004 return {nx.utils.arbitrary_element(G): center}
1005
1006 pos = []
1007 if equidistant:
1008 chord = 1
1009 step = 0.5
1010 theta = resolution
1011 for _ in range(len(G)):
1012 r = step * theta
1013 theta += chord / r
1014 pos.append([np.cos(theta) * r, np.sin(theta) * r])
1015
1016 else:
1017 # set the starting angle and step
1018 step = 1
1019 angle = 0.0
1020 dist = 0.0
1021 # set the radius for the spiral to the number of nodes in the graph
1022 radius = len(G)
1023
1024 while dist * np.hypot(np.cos(angle), np.sin(angle)) < radius:
1025 pos.append([dist * np.cos(angle), dist * np.sin(angle)])
1026 dist += step
1027 angle += resolution
1028
1029 pos = rescale_layout(np.array(pos), scale=scale) + center
1030
1031 pos = dict(zip(G, pos))
1032
1033 return pos
1034
1035
1036 def rescale_layout(pos, scale=1):
1037 """Returns scaled position array to (-scale, scale) in all axes.
1038
1039 The function acts on NumPy arrays which hold position information.
1040 Each position is one row of the array. The dimension of the space
1041 equals the number of columns. Each coordinate in one column.
1042
1043 To rescale, the mean (center) is subtracted from each axis separately.
1044 Then all values are scaled so that the largest magnitude value
1045 from all axes equals `scale` (thus, the aspect ratio is preserved).
1046 The resulting NumPy Array is returned (order of rows unchanged).
1047
1048 Parameters
1049 ----------
1050 pos : numpy array
1051 positions to be scaled. Each row is a position.
1052
1053 scale : number (default: 1)
1054 The size of the resulting extent in all directions.
1055
1056 Returns
1057 -------
1058 pos : numpy array
1059 scaled positions. Each row is a position.
1060
1061 """
1062 # Find max length over all dimensions
1063 lim = 0 # max coordinate for all axes
1064 for i in range(pos.shape[1]):
1065 pos[:, i] -= pos[:, i].mean()
1066 lim = max(abs(pos[:, i]).max(), lim)
1067 # rescale to (-scale, scale) in all directions, preserves aspect
1068 if lim > 0:
1069 for i in range(pos.shape[1]):
1070 pos[:, i] *= scale / lim
1071 return pos
1072
1073
1074 # fixture for pytest
1075 def setup_module(module):
1076 import pytest
1077 numpy = pytest.importorskip('numpy')
1078 scipy = pytest.importorskip('scipy')