Repository: networkx/networkx

Issue:
Inconsistent signatures for layout methods
I noticed some good additions have been made to layouts for the 1.10 version.

However, I also noticed that `random_layout` is the only method lacking the `scale` parameter.

```
random_layout(G, dim=2, center=None)
circular_layout(G, dim=2, scale=1, center=None)
```

The `center` parameter has been added in v1.10. Adding `scale` as well would have made it more intuitive to switch between the two layouts.

A similar argument could be made for other methods, the signatures are quite inconsistent.
Notice the order of parameters `dim`, `scale`, `center` and `weight`.

```
circular_layout(G[, dim, scale, center])
random_layout(G[, dim, center])
shell_layout(G[, nlist, dim, scale, center])
spring_layout(G, dim, k, pos, fixed, iterations, weight, scale, center)
spectral_layout(G, dim, weight, scale, center)
```

They could be re-organized for v2, putting parameters with the same name in the same place

```
circular_layout(G[, dim, scale, center])
random_layout(G[, dim, scale, center])
shell_layout(G[, dim, scale, center, nlist])
spring_layout(G[, dim, scale, center, weight, k, pos, fixed, iterations])
spectral_layout(G[, dim, scale, center, weight])
```

EDIT: another welcome addition would be a `seed` parameter for `random_layout` and possibly also for `spring_layout` (right now there's no way to make it deterministic).



Discussion:
I don't think we should push too hard on making the parameters the same for all the layout functions. In particular the `random_layout` was specifically chosen to be different. `circular_` and `shell_` naturally surround the origin, while `random_` and `spring_` are more natural on the unit square. I'm not sure `spectral_` should even have a "center" argument at all.

The `random_layout` is especially important to be clear that the size affects the range of "possible values" and not the range of actual results. If you choose uniformly random values on `[0,1)` and then rescale them so the max value is "size", then it's not easy to figure out the random distribution you chose from. Much better to choose uniformly random on `[0, size)`.

I think this ticket is really about aligning expectations of users with the code.  Users expect the arguments to be the same -- and we should be more clear that the layout range is different for different layouts. 

Perhaps the differences would be more clear if we used different names for the arguments in layouts which return circular results from those with square.  For example, "scale" and "center" for circular domains vs "size" and "shift" for square domains.

```
circular_layout(G, scale, center])
shell_layout(G, nlist, scale, center)
random_layout(G, size, shift)
spring_layout(G, weight, k, pos, fixed, iterations, size, shift)
```

I don't particularly care about the order of the parameters. But if we're going to go there then `dim` should not be the first argument. The default is almost always used. Perhaps the last three should be `scale, center, dim)` so that the layout specific arguments can be used without keywords: e.g. `spectral_layout(G, "weight")`.

I would propose (leaving spectral's return values as they are because I don't have a better suggestion):

```
circular_layout(G[, scale, center, dim])     --> [-scale, scale] + center
shell_layout(G[, nlist, scale, center, dim])

random_layout(G[, size, shift, dim])         ---> [0, size] + shift
spring_layout(G[, weight, k, pos, fixed, iterations, size, shift, dim])

spectral_layout(G[, weight, scale, center, dim])    ---> [-scale, scale] + center
```

@AgostinoSturaro what do you think?

Thank you for asking.

I started the topic when 1.10 was released and I noticed that I had to manually rescale some methods and not some others. That collided with my immediate expectations.

Now, if I stop and think about it, there is a reason for `random_layout` to be the only one without a `scale` parameter. But if we talk about expectations, that was definitely unexpected.
Having a reasonable `scale` (or `size`) parameter can be handy, and choosing the distribution in `[0, scale)`, or `[0, size)` should get reasonable results.
I agree that scaling the `random_layout` would skew the distribution, but the fact that there are both `dim` and `scale` parameters for layouts is the "problem" here. A note in the documentation could help distinguish between the role of the two parameters.

I see that some layouts produce a graph in `[-scale, scale]`, while others produce a graph in `[0, size]`, and this may not be very intuitive at first, but it's easy to see the effects by running the functions a couple of times.

I understand why you suggest using a `shift` parameter for `random_layout` instead of `center`.
However, the `center` parameter could have the same meaning for all layouts. It can default at the origin (0, 0) for `circular_layout` and at (0.5, 0.5) for `random_layout`.
It's true that (0.5, 0.5) is not the exact center of [0, 1), but it should fit expectations.
On the other hand, if `center` "shifts" the `random_layout`, then probably it should be called it shift ;)
I see no optimal solution. You decide what to do here.

About `dim`, up to v1.9.1, it was the first parameter for all layouts, except for `shell_layout`.

```
circular_layout(G[, dim, scale])
random_layout(G[, dim])
shell_layout(G[, nlist, dim, scale])  # here
spring_layout(G[, dim, k, pos, fixed, ...])
spectral_layout(G[, dim, weight, scale])
```

In fact, that tricked me when I used `shell_layout`, because I memorized `dim` to be in the second position, and so I expected it to be there.

To prevent breakages for a minor version (v1.11), I propose these changes only

```
circular_layout(G[, dim, scale, center])  # where center defaults to (0, 0)
random_layout(G[, dim, scale, center])  # where center defaults to (0.5, 0.5)
```

I agree on your idea of moving the less used parameters at the end.
This could be done for v2, to prevent API breakages, right?
Notice how, `shell_layout`, `spring_layout` and  `spectral_layout` all have `dim` and `scale` parameters, but in different order, and there is no particular reason for the different order.
Here is my proposal for them

```
shell_layout(G[, nlist, dim, scale, center])
spring_layout(G[, k, pos, fixed, iterations, weight, dim, scale, center])
spectral_layout(G[, weight, dim, scale, center])
```

With those changes, `dim, scale, center` would always be at the end, and we would gain consistency.
@dschult What do you think?

Does this need fixing for v1.11?

#1760 was merged back in Sept to v1.11 to fix this issue for that version. It added scale and center for all but random_layout which did not get the scale parameter (because scaling the points to be in a specific range makes the random process wonky--scaling the potential domain makes sense but differs from the other usage of the keyword `scale`). I plan to revisit this for v2, but thought we should not do the other changes on a minor release. 

Discussion came back to this issue when #1892 was submitted. but I think of it as being for v2 not for v1.11

The proposal for v1.11 above (two comments up) suggests changes to `circular_layout()` that have already been made and then to add `scale` to `random_layout()` which I had decided not to do because it is potentially confusing.


Resolving patch:
diff --git a/networkx/drawing/layout.py b/networkx/drawing/layout.py
--- a/networkx/drawing/layout.py
+++ b/networkx/drawing/layout.py
@@ -1,3 +1,12 @@
+#    Copyright (C) 2004-2016 by
+#    Aric Hagberg <hagberg@lanl.gov>
+#    Dan Schult <dschult@colgate.edu>
+#    Pieter Swart <swart@lanl.gov>
+#    All rights reserved.
+#    BSD license.
+#
+# Authors: Aric Hagberg <aric.hagberg@gmail.com>,
+#          Dan Schult <dschult@colgate.edu>
 """
 ******
 Layout
@@ -5,31 +14,30 @@
 
 Node positioning algorithms for graph drawing.
 
-The default scales and centering for these layouts are
-typically squares with side [0, 1] or [0, scale].
-The two circular layout routines (circular_layout and
-shell_layout) have size [-1, 1] or [-scale, scale].
-"""
-# Authors: Aric Hagberg <aric.hagberg@gmail.com>,
-#          Dan Schult <dschult@colgate.edu>
+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.
 
-#    Copyright (C) 2004-2016 by
-#    Aric Hagberg <hagberg@lanl.gov>
-#    Dan Schult <dschult@colgate.edu>
-#    Pieter Swart <swart@lanl.gov>
-#    All rights reserved.
-#    BSD license.
+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.
+
+"""
+from __future__ import division
 import collections
 import networkx as nx
 
 __all__ = ['circular_layout',
            'random_layout',
+           'rescale_layout',
            'shell_layout',
            'spring_layout',
            'spectral_layout',
            'fruchterman_reingold_layout']
 
-def process_params(G, center, dim):
+
+def _process_params(G, center, dim):
     # Some boilerplate code.
     import numpy as np
 
@@ -50,7 +58,7 @@ def process_params(G, center, dim):
     return G, center
 
 
-def random_layout(G, dim=2, center=None):
+def random_layout(G, center=None, dim=2):
     """Position nodes uniformly at random in the unit square.
 
     For every node, a position is generated by choosing each of dim
@@ -61,18 +69,18 @@ def random_layout(G, dim=2, center=None):
     Parameters
     ----------
     G : NetworkX graph or list of nodes
-       A position will be assigned to every node in G.
-
-    dim : int
-       Dimension of layout.
+        A position will be assigned to every node in G.
 
     center : array-like or None
-       Coordinate pair around which to center the layout.
+        Coordinate pair around which to center the layout.
+
+    dim : int
+        Dimension of layout.
 
     Returns
     -------
     pos : dict
-       A dictionary of positions keyed by node
+        A dictionary of positions keyed by node
 
     Examples
     --------
@@ -82,7 +90,7 @@ def random_layout(G, dim=2, center=None):
     """
     import numpy as np
 
-    G, center = process_params(G, center, dim)
+    G, center = _process_params(G, center, dim)
     shape = (len(G), dim)
     pos = np.random.random(shape) + center
     pos = pos.astype(np.float32)
@@ -91,7 +99,7 @@ def random_layout(G, dim=2, center=None):
     return pos
 
 
-def circular_layout(G, dim=2, scale=1, center=None):
+def circular_layout(G, scale=1, center=None, dim=2):
     # dim=2 only
     """Position nodes on a circle.
 
@@ -99,24 +107,24 @@ def circular_layout(G, dim=2, scale=1, center=None):
     ----------
     G : NetworkX graph or list of nodes
 
-    dim : int
-       Dimension of layout, currently only dim=2 is supported
-
     scale : float
         Scale factor for positions
 
     center : array-like or None
-       Coordinate pair around which to center the layout.
+        Coordinate pair around which to center the layout.
+
+    dim : int
+        Dimension of layout, currently only dim=2 is supported
 
     Returns
     -------
-    dict :
-       A dictionary of positions keyed by node
+    pos : dict
+        A dictionary of positions keyed by node
 
     Examples
     --------
-    >>> G=nx.path_graph(4)
-    >>> pos=nx.circular_layout(G)
+    >>> G = nx.path_graph(4)
+    >>> pos = nx.circular_layout(G)
 
     Notes
     -----
@@ -126,7 +134,7 @@ def circular_layout(G, dim=2, scale=1, center=None):
     """
     import numpy as np
 
-    G, center = process_params(G, center, dim)
+    G, center = _process_params(G, center, dim)
 
     if len(G) == 0:
         pos = {}
@@ -137,12 +145,13 @@ def circular_layout(G, dim=2, scale=1, center=None):
         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)])
-        pos = _rescale_layout(pos, scale=scale) + center
+        pos = rescale_layout(pos, scale=scale) + center
         pos = dict(zip(G, pos))
 
     return pos
 
-def shell_layout(G, nlist=None, dim=2, scale=1, center=None):
+
+def shell_layout(G, nlist=None, scale=1, center=None, dim=2):
     """Position nodes in concentric circles.
 
     Parameters
@@ -152,24 +161,24 @@ def shell_layout(G, nlist=None, dim=2, scale=1, center=None):
     nlist : list of lists
        List of node lists for each shell.
 
-    dim : int
-       Dimension of layout, currently only dim=2 is supported
-
     scale : float
         Scale factor for positions
 
     center : array-like or None
-       Coordinate pair around which to center the layout.
+        Coordinate pair around which to center the layout.
+
+    dim : int
+        Dimension of layout, currently only dim=2 is supported
 
     Returns
     -------
-    dict :
-       A dictionary of positions keyed by node
+    pos : dict
+        A dictionary of positions keyed by node
 
     Examples
     --------
     >>> G = nx.path_graph(4)
-    >>> shells = [[0], [1,2,3]]
+    >>> shells = [[0], [1, 2, 3]]
     >>> pos = nx.shell_layout(G, shells)
 
     Notes
@@ -180,14 +189,13 @@ def shell_layout(G, nlist=None, dim=2, scale=1, center=None):
     """
     import numpy as np
 
-    G, center = process_params(G, center, dim)
+    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)]
@@ -199,50 +207,48 @@ def shell_layout(G, nlist=None, dim=2, scale=1, center=None):
         # else start at r=1
         radius = 1.0
 
-    npos={}
+    npos = {}
     for nodes in nlist:
         # Discard the extra angle since it matches 0 radians.
         theta = np.linspace(0, 1, len(nodes) + 1)[:-1] * 2 * np.pi
         theta = theta.astype(np.float32)
         pos = np.column_stack([np.cos(theta), np.sin(theta)])
-        pos = _rescale_layout(pos, scale=scale * radius / len(nlist)) + center
+        pos = rescale_layout(pos, scale=scale * radius / len(nlist)) + center
         npos.update(zip(nodes, pos))
         radius += 1.0
 
     return npos
 
 
-def fruchterman_reingold_layout(G,dim=2,k=None,
+def fruchterman_reingold_layout(G, k=None,
                                 pos=None,
                                 fixed=None,
                                 iterations=50,
                                 weight='weight',
                                 scale=1.0,
-                                center=None):
+                                center=None,
+                                dim=2):
     """Position nodes using Fruchterman-Reingold force-directed algorithm.
 
     Parameters
     ----------
     G : NetworkX graph or list of nodes
 
-    dim : int
-       Dimension of layout
-
     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.
+        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 list or tuple.  If None, then use random initial
-       positions.
+        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 to keep fixed at initial position.
 
     iterations : int  optional (default=50)
-       Number of iterations of spring-force relaxation
+        Number of iterations of spring-force relaxation
 
     weight : string or None   optional (default='weight')
         The edge attribute that holds the numerical value used for
@@ -250,27 +256,30 @@ def fruchterman_reingold_layout(G,dim=2,k=None,
 
     scale : float (default=1.0)
         Scale factor for positions. The nodes are positioned
-        in a box of size [0,scale] x [0,scale].
+        in a box of size [0, scale] x [0, scale].
 
     center : array-like or None
-       Coordinate pair around which to center the layout.
+        Coordinate pair around which to center the layout.
+
+    dim : int
+        Dimension of layout
 
     Returns
     -------
-    dict :
-       A dictionary of positions keyed by node
+    pos : dict
+        A dictionary of positions keyed by node
 
     Examples
     --------
-    >>> G=nx.path_graph(4)
-    >>> pos=nx.spring_layout(G)
+    >>> G = nx.path_graph(4)
+    >>> pos = nx.spring_layout(G)
 
-    # The same using longer function name
-    >>> pos=nx.fruchterman_reingold_layout(G)
+    # The same using longer but equivalent function name
+    >>> pos = nx.fruchterman_reingold_layout(G)
     """
     import numpy as np
 
-    G, center = process_params(G, center, dim)
+    G, center = _process_params(G, center, dim)
 
     if fixed is not None:
         nfixed = dict(zip(G, range(len(G))))
@@ -278,14 +287,14 @@ def fruchterman_reingold_layout(G,dim=2,k=None,
 
     if pos is not None:
         # Determine size of existing domain to adjust initial positions
-        dom_size = max(flatten(pos.values()))
+        dom_size = max(coord for coord in pos_tup for pos_tup in pos.values())
         shape = (len(G), dim)
         pos_arr = np.random.random(shape) * dom_size + center
-        for i,n in enumerate(G):
+        for i, n in enumerate(G):
             if n in pos:
                 pos_arr[i] = np.asarray(pos[n])
     else:
-        pos_arr=None
+        pos_arr = None
 
     if len(G) == 0:
         return {}
@@ -296,53 +305,58 @@ def fruchterman_reingold_layout(G,dim=2,k=None,
         # Sparse matrix
         if len(G) < 500:  # sparse solver for large graphs
             raise ValueError
-        A = nx.to_scipy_sparse_matrix(G,weight=weight,dtype='f')
+        A = nx.to_scipy_sparse_matrix(G, weight=weight, dtype='f')
         if k is None and fixed is not None:
-           # We must adjust k by domain size for layouts that are not near 1x1
-           nnodes,_ = A.shape
-           k = dom_size / np.sqrt(nnodes)
-        pos = _sparse_fruchterman_reingold(A, dim, k, pos_arr, fixed, iterations)
+            # 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, dim)
     except:
-        A = nx.to_numpy_matrix(G,weight=weight)
+        A = nx.to_numpy_matrix(G, weight=weight)
         if k is None and fixed is not None:
-           # We must adjust k by domain size for layouts that are not near 1x1
-           nnodes,_ = A.shape
-           k = dom_size / np.sqrt(nnodes)
-        pos = _fruchterman_reingold(A, dim, k, pos_arr, fixed, iterations)
+            # 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, dim)
     if fixed is None:
-        pos = _rescale_layout(pos, scale=scale) + center
-    pos = dict(zip(G,pos))
+        pos = rescale_layout(pos, scale=scale) + center
+    pos = dict(zip(G, pos))
     return pos
 
-spring_layout=fruchterman_reingold_layout
 
-def _fruchterman_reingold(A, dim=2, k=None, pos=None, fixed=None,
-                          iterations=50):
+spring_layout = fruchterman_reingold_layout
+
+
+def _fruchterman_reingold(A, k=None, pos=None, fixed=None,
+                          iterations=50, dim=2):
     # Position nodes in adjacency matrix A using Fruchterman-Reingold
     # Entry point for NetworkX graph is fruchterman_reingold_layout()
     try:
         import numpy as np
     except ImportError:
-        raise ImportError("_fruchterman_reingold() requires numpy: http://scipy.org/ ")
+        msg = "_fruchterman_reingold() requires numpy: http://scipy.org/ "
+        raise ImportError(msg)
 
     try:
-        nnodes,_=A.shape
+        nnodes, _ = A.shape
     except AttributeError:
-        raise nx.NetworkXError(
-            "fruchterman_reingold() takes an adjacency matrix as input")
+        msg = "fruchterman_reingold() takes an adjacency matrix as input"
+        raise nx.NetworkXError(msg)
 
-    A=np.asarray(A) # make sure we have an array instead of a matrix
+    # make sure we have an array instead of a matrix
+    A = np.asarray(A)
 
     if pos is None:
         # random initial positions
-        pos=np.asarray(np.random.random((nnodes,dim)),dtype=A.dtype)
+        pos = np.asarray(np.random.random((nnodes, dim)), dtype=A.dtype)
     else:
         # make sure positions are of same type as matrix
-        pos=pos.astype(A.dtype)
+        pos = pos.astype(A.dtype)
 
     # optimal distance between nodes
     if k is None:
-        k=np.sqrt(1.0/nnodes)
+        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
@@ -350,118 +364,117 @@ def _fruchterman_reingold(A, dim=2, k=None, pos=None, fixed=None,
     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/float(iterations+1)
-    delta = np.zeros((pos.shape[0],pos.shape[0],pos.shape[1]),dtype=A.dtype)
+    dt = t/float(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
         for i in range(pos.shape[1]):
-            delta[:,:,i]= pos[:,i,None]-pos[:,i]
+            delta[:, :, i] = pos[:, i, None] - pos[:, i]
         # distance between points
-        distance=np.sqrt((delta**2).sum(axis=-1))
+        distance = np.sqrt((delta**2).sum(axis=-1))
         # enforce minimum distance of 0.01
-        distance=np.where(distance<0.01,0.01,distance)
+        distance = np.where(distance < 0.01, 0.01, distance)
         # displacement "force"
-        displacement=np.transpose(np.transpose(delta)*\
-                                  (k*k/distance**2-A*distance/k))\
-                                  .sum(axis=1)
+        displacement = np.transpose(np.transpose(delta) *
+                                    (k * k / distance**2 - A * distance / k)
+                                    ).sum(axis=1)
         # update positions
-        length=np.sqrt((displacement**2).sum(axis=1))
-        length=np.where(length<0.01,0.1,length)
-        delta_pos=np.transpose(np.transpose(displacement)*t/length)
+        length = np.sqrt((displacement**2).sum(axis=1))
+        length = np.where(length < 0.01, 0.1, length)
+        delta_pos = np.transpose(np.transpose(displacement) * t / length)
         if fixed is not None:
             # don't change positions of fixed nodes
-            delta_pos[fixed]=0.0
-        pos+=delta_pos
+            delta_pos[fixed] = 0.0
+        pos += delta_pos
         # cool temperature
-        t-=dt
+        t -= dt
     return pos
 
 
-def _sparse_fruchterman_reingold(A, dim=2, k=None, pos=None, fixed=None,
-                                 iterations=50):
+def _sparse_fruchterman_reingold(A, k=None, pos=None, fixed=None,
+                                 iterations=50, dim=2):
     # Position nodes in adjacency matrix A using Fruchterman-Reingold
     # Entry point for NetworkX graph is fruchterman_reingold_layout()
     # Sparse version
     try:
         import numpy as np
     except ImportError:
-        raise ImportError("_sparse_fruchterman_reingold() requires numpy: http://scipy.org/ ")
+        m = "_sparse_fruchterman_reingold() requires numpy: http://scipy.org/"
+        raise ImportError(m)
     try:
-        nnodes,_=A.shape
+        nnodes, _ = A.shape
     except AttributeError:
-        raise nx.NetworkXError(
-            "fruchterman_reingold() takes an adjacency matrix as input")
+        msg = "fruchterman_reingold() takes an adjacency matrix as input"
+        raise nx.NetworkXError(msg)
     try:
-        from scipy.sparse import spdiags,coo_matrix
+        from scipy.sparse import spdiags, coo_matrix
     except ImportError:
-        raise ImportError("_sparse_fruchterman_reingold() scipy numpy: http://scipy.org/ ")
+        msg = "_sparse_fruchterman_reingold() scipy numpy: http://scipy.org/ "
+        raise ImportError(msg)
     # make sure we have a LIst of Lists representation
     try:
-        A=A.tolil()
+        A = A.tolil()
     except:
-        A=(coo_matrix(A)).tolil()
+        A = (coo_matrix(A)).tolil()
 
     if pos is None:
         # random initial positions
-        pos=np.asarray(np.random.random((nnodes,dim)),dtype=A.dtype)
+        pos = np.asarray(np.random.random((nnodes, dim)), dtype=A.dtype)
     else:
         # make sure positions are of same type as matrix
-        pos=pos.astype(A.dtype)
+        pos = pos.astype(A.dtype)
 
     # no fixed nodes
     if fixed is None:
-        fixed=[]
+        fixed = []
 
     # optimal distance between nodes
     if k is None:
-        k=np.sqrt(1.0/nnodes)
+        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.
-    t=0.1
+    t = 0.1
     # simple cooling scheme.
     # linearly step down by dt on each iteration so last iteration is size dt.
-    dt=t/float(iterations+1)
+    dt = t / float(iterations+1)
 
-    displacement=np.zeros((dim,nnodes))
+    displacement = np.zeros((dim, nnodes))
     for iteration in range(iterations):
-        displacement*=0
+        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
+            delta = (pos[i] - pos).T
             # distance between points
-            distance=np.sqrt((delta**2).sum(axis=0))
+            distance = np.sqrt((delta**2).sum(axis=0))
             # enforce minimum distance of 0.01
-            distance=np.where(distance<0.01,0.01,distance)
+            distance = np.where(distance < 0.01, 0.01, distance)
             # the adjacency matrix row
-            Ai=np.asarray(A.getrowview(i).toarray())
+            Ai = np.asarray(A.getrowview(i).toarray())
             # displacement "force"
-            displacement[:,i]+=\
-                (delta*(k*k/distance**2-Ai*distance/k)).sum(axis=1)
+            displacement[:, i] +=\
+                (delta * (k * k / distance**2 - Ai * distance / k)).sum(axis=1)
         # update positions
-        length=np.sqrt((displacement**2).sum(axis=0))
-        length=np.where(length<0.01,0.1,length)
-        pos+=(displacement*t/length).T
+        length = np.sqrt((displacement**2).sum(axis=0))
+        length = np.where(length < 0.01, 0.1, length)
+        pos += (displacement * t / length).T
         # cool temperature
-        t-=dt
+        t -= dt
     return pos
 
 
-def spectral_layout(G, dim=2, weight='weight', scale=1, center=None):
+def spectral_layout(G, weight='weight', scale=1, center=None, dim=2):
     """Position nodes using the eigenvectors of the graph Laplacian.
 
     Parameters
     ----------
     G : NetworkX graph or list of nodes
 
-    dim : int
-       Dimension of layout
-
     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.
@@ -470,17 +483,20 @@ def spectral_layout(G, dim=2, weight='weight', scale=1, center=None):
         Scale factor for positions
 
     center : array-like or None
-       Coordinate pair around which to center the layout.
+        Coordinate pair around which to center the layout.
+
+    dim : int
+        Dimension of layout
 
     Returns
     -------
-    dict :
-       A dictionary of positions keyed by node
+    pos : dict
+        A dictionary of positions keyed by node
 
     Examples
     --------
-    >>> G=nx.path_graph(4)
-    >>> pos=nx.spectral_layout(G)
+    >>> G = nx.path_graph(4)
+    >>> pos = nx.spectral_layout(G)
 
     Notes
     -----
@@ -493,7 +509,7 @@ def spectral_layout(G, dim=2, weight='weight', scale=1, center=None):
     # handle some special cases that break the eigensolvers
     import numpy as np
 
-    G, center = process_params(G, center, dim)
+    G, center = _process_params(G, center, dim)
 
     if len(G) <= 2:
         if len(G) == 0:
@@ -502,16 +518,16 @@ def spectral_layout(G, dim=2, weight='weight', scale=1, center=None):
             pos = np.array([center])
         else:
             pos = np.array([np.zeros(dim), np.array(center)*2.0])
-        return dict(zip(G,pos))
+        return dict(zip(G, pos))
     try:
         # Sparse matrix
-        if len(G)< 500:  # dense solver is faster for small graphs
+        if len(G) < 500:  # dense solver is faster for small graphs
             raise ValueError
         A = nx.to_scipy_sparse_matrix(G, weight=weight, dtype='d')
         # Symmetrize directed graphs
         if G.is_directed():
             A = A + np.transpose(A)
-        pos = _sparse_spectral(A,dim)
+        pos = _sparse_spectral(A, dim)
     except (ImportError, ValueError):
         # Dense matrix
         A = nx.to_numpy_matrix(G, weight=weight)
@@ -520,8 +536,8 @@ def spectral_layout(G, dim=2, weight='weight', scale=1, center=None):
             A = A + np.transpose(A)
         pos = _spectral(A, dim)
 
-    pos = _rescale_layout(pos, scale) + center
-    pos = dict(zip(G,pos))
+    pos = rescale_layout(pos, scale) + center
+    pos = dict(zip(G, pos))
     return pos
 
 
@@ -531,26 +547,28 @@ def _spectral(A, dim=2):
     try:
         import numpy as np
     except ImportError:
-        raise ImportError("spectral_layout() requires numpy: http://scipy.org/ ")
+        msg = "spectral_layout() requires numpy: http://scipy.org/ "
+        raise ImportError(msg)
     try:
-        nnodes,_=A.shape
+        nnodes, _ = A.shape
     except AttributeError:
-        raise nx.NetworkXError(\
-            "spectral() takes an adjacency matrix as input")
+        msg = "spectral() takes an adjacency matrix as input"
+        raise nx.NetworkXError(msg)
 
     # form Laplacian matrix
     # make sure we have an array instead of a matrix
-    A=np.asarray(A)
-    I=np.identity(nnodes,dtype=A.dtype)
-    D=I*np.sum(A,axis=1) # diagonal of degrees
-    L=D-A
+    A = np.asarray(A)
+    I = np.identity(nnodes, dtype=A.dtype)
+    D = I * np.sum(A, axis=1)  # diagonal of degrees
+    L = D - A
 
-    eigenvalues,eigenvectors=np.linalg.eig(L)
+    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])
+    index = np.argsort(eigenvalues)[1:dim + 1]  # 0 index is zero eigenvalue
+    return np.real(eigenvectors[:, index])
+
 
-def _sparse_spectral(A,dim=2):
+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"
@@ -558,44 +576,68 @@ def _sparse_spectral(A,dim=2):
         import numpy as np
         from scipy.sparse import spdiags
     except ImportError:
-        raise ImportError("_sparse_spectral() requires scipy & numpy: http://scipy.org/ ")
+        msg = "_sparse_spectral() requires scipy & numpy: http://scipy.org/ "
+        raise ImportError(msg)
     try:
         from scipy.sparse.linalg.eigen import eigsh
     except ImportError:
         # scipy <0.9.0 names eigsh differently
         from scipy.sparse.linalg import eigen_symmetric as eigsh
     try:
-        nnodes,_=A.shape
+        nnodes, _ = A.shape
     except AttributeError:
-        raise nx.NetworkXError(\
-            "sparse_spectral() takes an adjacency matrix as input")
+        msg = "sparse_spectral() takes an adjacency matrix as input"
+        raise nx.NetworkXError(msg)
 
     # form Laplacian matrix
-    data=np.asarray(A.sum(axis=1).T)
-    D=spdiags(data,0,nnodes,nnodes)
-    L=D-A
+    data = np.asarray(A.sum(axis=1).T)
+    D = spdiags(data, 0, nnodes, nnodes)
+    L = D - A
 
-    k=dim+1
+    k = dim + 1
     # number of Lanczos vectors for ARPACK solver.What is the right scaling?
-    ncv=max(2*k+1,int(np.sqrt(nnodes)))
+    ncv = max(2 * k + 1, int(np.sqrt(nnodes)))
     # return smallest k eigenvalues and eigenvectors
-    eigenvalues,eigenvectors=eigsh(L,k,which='SM',ncv=ncv)
-    index=np.argsort(eigenvalues)[1:k] # 0 index is zero eigenvalue
-    return np.real(eigenvectors[:,index])
+    eigenvalues, eigenvectors = eigsh(L, k, which='SM', ncv=ncv)
+    index = np.argsort(eigenvalues)[1:k]  # 0 index is zero eigenvalue
+    return np.real(eigenvectors[:, index])
+
+
+def rescale_layout(pos, scale=1):
+    """Return 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.
 
-def _rescale_layout(pos,scale=1):
-    # rescale to (-scale,scale) in all axes
+    scale : number (default: 1)
+        The size of the resulting extent in all directions.
 
+    Returns
+    -------
+    pos : numpy array
+        scaled positions. Each row is a position.
+
+    """
     # Find max length over all dimensions
-    lim=0 # max coordinate for all axes
+    lim = 0  # max coordinate for all axes
     for i in range(pos.shape[1]):
-        pos[:,i]-=pos[:,i].mean()
-        lim=max(pos[:,i].max(),lim)
-    # rescale to (-scale,scale) in all directions, preserves aspect
+        pos[:, i] -= pos[:, i].mean()
+        lim = max(pos[:, i].max(), lim)
+    # rescale to (-scale, scale) in all directions, preserves aspect
     if lim > 0:
         for i in range(pos.shape[1]):
-            pos[:,i]*=scale/lim
+            pos[:, i] *= scale / lim
     return pos
 
 
@@ -610,17 +652,3 @@ def setup_module(module):
         import scipy
     except:
         raise SkipTest("SciPy not available")
-
-def flatten(l):
-    try:
-        bs = basestring
-    except NameError:
-        # Py3k
-        bs = str
-    for el in l:
-        if isinstance(el, collections.Iterable) and not isinstance(el, bs):
-            for sub in flatten(el):
-                yield sub
-        else:
-            yield el
-


Test patch: