========================================================
Representing spatial structure and calculating distances
========================================================

The ``space`` module contains classes for specifying the locations of neurons
in space and for calculating the distances between them.

Neuron positions can be defined either manually, using the ``positions``
attribute of a ``Population`` or using a ``Structure`` instance which is passed
to the ``Population`` constructor.

A number of different structures are available in ``space``. It is simple to
define your own ``Structure`` sub-class if you need something that is not
already provided.

The simplest structure is a grid, whether 1D, 2D or 3D, e.g.::
    
    >>> from pyNN.space import *
    >>> line = Line(dx=100.0, x0=0.0, y=200.0, z=500.0)
    >>> line.generate_positions(7)
    array([[   0., 100., 200., 300., 400., 500., 600.],
           [ 200., 200., 200., 200., 200., 200., 200.],
           [ 500., 500., 500., 500., 500., 500., 500.]])
    >>> grid = Grid2D(aspect_ratio=3, dx=10.0, dy=25.0, z=-3.0)
    >>> grid.generate_positions(3)
    array([[  0., 10., 20.],
           [  0.,  0.,  0.],
           [ -3., -3., -3.]])
    >>> grid.generate_positions(12)
    array([[  0.,  0., 10., 10., 20., 20., 30., 30., 40., 40., 50., 50.],
           [  0., 25.,  0., 25.,  0., 25.,  0., 25.,  0., 25.,  0., 25.],
           [ -3., -3., -3., -3., -3., -3., -3., -3., -3., -3., -3., -3.]])

Here we have specified an *x*:*y* ratio of 3, so if we ask the grid to
generate positions for 3 neurons, we get a 3x1 grid, 12 neurons a 6x2 grid,
27 neurons 9x3, etc.

BY default, grid positions are filled sequentially, iterating first over the *z*
dimension, then *y*, then *x*, but we can also fill the grid randomly::

    >>> rgrid = Grid2D(aspect_ratio=1, dx=10.0, dy=10.0, fill_order='random')
    >>> rgrid.generate_positions(9)
    array([[ 20., 20., 20.,  0., 10.,  0., 10.,  0., 10.],
           [ 10.,  0., 20., 10.,  0., 20., 10.,  0., 20.],
           [  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.]])

The ``space`` module also provides the ``RandomStructure`` class, which
distributes neurons randomly and uniformly within a given volume::

    >>> glomerulus = RandomStructure(boundary=Sphere(radius=200.0))
    >>> glomerulus.generate_positions(5)
    array([[  4.81853231e+01, -2.49317729e+01,  1.08294461e+02,  1.72125819e-01,  -1.25552649e+02],
           [  3.96588073e+01,  1.75426143e+02,  3.19290169e+01,  1.65050459e+02,  -1.32092198e+00],
           [ -1.00801053e+02, -8.51701627e+01, -1.39804442e+02, -4.97765369e+01,   3.94241050e+01]])

The volume classes currently available are ``Sphere`` and ``Cuboid``.

Defining your own ``Structure`` classes is straightforward, just inherit from
``BaseStructure`` and implement a ``generate_positions(n)`` method::

    class MyStructure(BaseStructure):
        parameter_names = ("spam", "eggs")
       
        def __init__(self, spam=3, eggs=1):
            ...
          
        def generate_positions(self, n):
            ...
            # must return a 3xn numpy array
            
To definite your own ``Shape`` class for use with ``RandomStructure``, subclass
``Shape`` and implement a ``sample(n, rng)`` method::

    class Tetrahedron(Shape):
    
        def __init__(self, side_length):
            ...
            
        def sample(self, n, rng):
           ...
           # return a nx3 numpy array.

Note that rotation of structures is currently missing, but will be implemented
in the next release.
    
