| 1 | # encoding: utf-8 |
|---|
| 2 | """ |
|---|
| 3 | Tools for performing spatial/topographical calculations. |
|---|
| 4 | |
|---|
| 5 | """ |
|---|
| 6 | |
|---|
| 7 | # There must be some Python package out there that provides most of this stuff. |
|---|
| 8 | |
|---|
| 9 | import numpy |
|---|
| 10 | import math |
|---|
| 11 | from operator import and_ |
|---|
| 12 | from pyNN.random import NumpyRNG |
|---|
| 13 | |
|---|
| 14 | def distance(src, tgt, mask=None, scale_factor=1.0, offset=0.0, |
|---|
| 15 | periodic_boundaries=None): # may need to add an offset parameter |
|---|
| 16 | """ |
|---|
| 17 | Return the Euclidian distance between two cells. |
|---|
| 18 | `mask` allows only certain dimensions to be considered, e.g.:: |
|---|
| 19 | * to ignore the z-dimension, use `mask=array([0,1])` |
|---|
| 20 | * to ignore y, `mask=array([0,2])` |
|---|
| 21 | * to just consider z-distance, `mask=array([2])` |
|---|
| 22 | `scale_factor` allows for different units in the pre- and post- position |
|---|
| 23 | (the post-synaptic position is multipied by this quantity). |
|---|
| 24 | """ |
|---|
| 25 | d = src.position - scale_factor*(tgt.position + offset) |
|---|
| 26 | |
|---|
| 27 | if not periodic_boundaries == None: |
|---|
| 28 | d = numpy.minimum(abs(d), periodic_boundaries-abs(d)) |
|---|
| 29 | if mask is not None: |
|---|
| 30 | d = d[mask] |
|---|
| 31 | return numpy.sqrt(numpy.dot(d, d)) |
|---|
| 32 | |
|---|
| 33 | |
|---|
| 34 | class Space(object): |
|---|
| 35 | |
|---|
| 36 | AXES = {'x' : [0], 'y': [1], 'z': [2], |
|---|
| 37 | 'xy': [0,1], 'yz': [1,2], 'xz': [0,2], 'xyz': range(3), None: range(3)} |
|---|
| 38 | |
|---|
| 39 | def __init__(self, axes=None, scale_factor=1.0, offset=0.0, |
|---|
| 40 | periodic_boundaries=None): |
|---|
| 41 | """ |
|---|
| 42 | axes -- if not supplied, then the 3D distance is calculated. If supplied, |
|---|
| 43 | axes should be a string containing the axes to be used, e.g. 'x', or |
|---|
| 44 | 'yz'. axes='xyz' is the same as axes=None. |
|---|
| 45 | scale_factor -- it may be that the pre and post populations use |
|---|
| 46 | different units for position, e.g. degrees and µm. In this case, |
|---|
| 47 | `scale_factor` can be specified, which is applied to the positions |
|---|
| 48 | in the post-synaptic population. |
|---|
| 49 | offset -- if the origins of the coordinate systems of the pre- and post- |
|---|
| 50 | synaptic populations are different, `offset` can be used to adjust |
|---|
| 51 | for this difference. The offset is applied before any scaling. |
|---|
| 52 | periodic_boundaries -- either `None`, or a tuple giving the boundaries |
|---|
| 53 | for each dimension, e.g. `((x_min, x_max), None, (z_min, z_max))`. |
|---|
| 54 | """ |
|---|
| 55 | self.periodic_boundaries = periodic_boundaries |
|---|
| 56 | self.axes = numpy.array(Space.AXES[axes]) |
|---|
| 57 | self.scale_factor = scale_factor |
|---|
| 58 | self.offset = offset |
|---|
| 59 | |
|---|
| 60 | def distances(self, A, B, expand=False): |
|---|
| 61 | """ |
|---|
| 62 | Calculate the distance matrix between two sets of coordinates, given |
|---|
| 63 | the topology of the current space. |
|---|
| 64 | From http://projects.scipy.org/pipermail/numpy-discussion/2007-April/027203.html |
|---|
| 65 | """ |
|---|
| 66 | if len(A.shape) == 1: |
|---|
| 67 | A = A.reshape(3, 1) |
|---|
| 68 | if len(B.shape) == 1: |
|---|
| 69 | B = B.reshape(3, 1) |
|---|
| 70 | # I'm not sure the following line should be here. Operations may be redundant and not very |
|---|
| 71 | # transparent from the user point of view. I moved it into the DistanceDependentProbability Connector |
|---|
| 72 | #B = self.scale_factor*(B + self.offset) |
|---|
| 73 | d = numpy.zeros((len(self.axes), A.shape[1], B.shape[1]), dtype=A.dtype) |
|---|
| 74 | for axis in self.axes: |
|---|
| 75 | diff2 = A[axis,:,None] - B[axis, :] |
|---|
| 76 | if self.periodic_boundaries is not None: |
|---|
| 77 | boundaries = self.periodic_boundaries[axis] |
|---|
| 78 | if boundaries is not None: |
|---|
| 79 | range = boundaries[1]-boundaries[0] |
|---|
| 80 | ad2 = abs(diff2) |
|---|
| 81 | diff2 = numpy.minimum(ad2, range-ad2) |
|---|
| 82 | diff2 **= 2 |
|---|
| 83 | d[axis] = diff2 |
|---|
| 84 | if not expand: |
|---|
| 85 | d = numpy.sum(d, 0) |
|---|
| 86 | numpy.sqrt(d, d) |
|---|
| 87 | return d |
|---|
| 88 | |
|---|
| 89 | |
|---|
| 90 | |
|---|
| 91 | class PositionsGenerator(object): |
|---|
| 92 | |
|---|
| 93 | def __init__(self, dimensions, axes=None): |
|---|
| 94 | """ |
|---|
| 95 | dimensions -- either `None`, or a tuple/list giving the dimensions |
|---|
| 96 | for each dimension, e.g. `((x_min, x_max), None, (z_min, z_max))`. |
|---|
| 97 | axes -- if not supplied, then the 3D distance is calculated. If supplied, |
|---|
| 98 | axes should be a string containing the axes to be used, e.g. 'x', or |
|---|
| 99 | 'yz'. axes='xyz' is the same as axes=None. |
|---|
| 100 | """ |
|---|
| 101 | self.dimensions = list(dimensions) |
|---|
| 102 | self.axes = numpy.array(Space.AXES[axes]) |
|---|
| 103 | assert len(self.dimensions) == 3, "Dimensions should be of size 3, and axes should specify orientation!" |
|---|
| 104 | for item in self.dimensions: |
|---|
| 105 | if item is not None: |
|---|
| 106 | assert len(item) == 2, "dimensions should be a list of tuples (min, max), not %s" %item |
|---|
| 107 | assert item[0] <= item[0], "items elements should be (min, max), with min <= max" |
|---|
| 108 | |
|---|
| 109 | def get(self, dims): |
|---|
| 110 | self.M = numpy.prod(numpy.array(dims)) |
|---|
| 111 | self.positions = numpy.zeros((3, self.M)) |
|---|
| 112 | pass |
|---|
| 113 | |
|---|
| 114 | |
|---|
| 115 | class RandomPositions(PositionsGenerator): |
|---|
| 116 | |
|---|
| 117 | def __init__(self, dimensions, seed=None): |
|---|
| 118 | PositionsGenerator.__init__(self, dimensions) |
|---|
| 119 | self.seed = seed |
|---|
| 120 | |
|---|
| 121 | def get(self, dims): |
|---|
| 122 | PositionsGenerator.get(self, dims) |
|---|
| 123 | numpy.random.seed(self.seed) |
|---|
| 124 | for axis in self.axes: |
|---|
| 125 | item = self.dimensions[axis] |
|---|
| 126 | if item is not None: |
|---|
| 127 | bmin, bmax = item |
|---|
| 128 | self.positions[axis, :] = bmin + bmax * numpy.random.rand(self.M) |
|---|
| 129 | return self.positions |
|---|
| 130 | |
|---|
| 131 | |
|---|
| 132 | class GridPositions(PositionsGenerator): |
|---|
| 133 | |
|---|
| 134 | def get(self, dims): |
|---|
| 135 | PositionsGenerator.get(self, dims) |
|---|
| 136 | res = "" |
|---|
| 137 | for d in dims: |
|---|
| 138 | res += "0:%d," %d |
|---|
| 139 | grid = eval("numpy.mgrid[%s]" %res[0:-1]) |
|---|
| 140 | for axis in self.axes: |
|---|
| 141 | item = self.dimensions[axis] |
|---|
| 142 | if item is not None: |
|---|
| 143 | bmin, bmax = item |
|---|
| 144 | padding = (bmax-bmin)/float(dims[axis]) |
|---|
| 145 | data = numpy.linspace(bmin+padding, bmax, dims[axis]) |
|---|
| 146 | self.positions[axis, :] = data[grid[axis].flatten()] |
|---|
| 147 | return self.positions |
|---|
| 148 | |
|---|
| 149 | |
|---|
| 150 | class BaseStructure(object): |
|---|
| 151 | pass |
|---|
| 152 | |
|---|
| 153 | |
|---|
| 154 | class Line(BaseStructure): |
|---|
| 155 | |
|---|
| 156 | def __init__(self, dx=1.0, x0=0.0, y0=0.0, z0=0.0): |
|---|
| 157 | self.dx = dx |
|---|
| 158 | self.x0 = x0 |
|---|
| 159 | self.y0 = y0 |
|---|
| 160 | self.z0 = z0 |
|---|
| 161 | |
|---|
| 162 | def __eq__(self, other): |
|---|
| 163 | return reduce(and_, (getattr(self, attr) == getattr(other, attr) |
|---|
| 164 | for attr in ("dx", "x0", "y0", "z0"))) |
|---|
| 165 | |
|---|
| 166 | def generate_positions(self, n): |
|---|
| 167 | x = self.dx*numpy.arange(n, dtype=float) + self.x0 |
|---|
| 168 | y = numpy.zeros(n) + self.y0 |
|---|
| 169 | z = numpy.zeros(n) + self.z0 |
|---|
| 170 | return numpy.array((x,y,z)) |
|---|
| 171 | |
|---|
| 172 | def describe(self, n): |
|---|
| 173 | return "line with %d positions" % n |
|---|
| 174 | |
|---|
| 175 | |
|---|
| 176 | class Grid2D(BaseStructure): |
|---|
| 177 | |
|---|
| 178 | def __init__(self, aspect_ratio=1.0, dx=1.0, dy=1.0, x0=0.0, y0=0.0, z=0, fill_order="sequential"): |
|---|
| 179 | """ |
|---|
| 180 | aspect_ratio - ratio of the number of grid points per side (not the ratio |
|---|
| 181 | of the side lengths, unless dx == dy) |
|---|
| 182 | """ |
|---|
| 183 | self.aspect_ratio = aspect_ratio |
|---|
| 184 | assert fill_order in ('sequential', 'random') |
|---|
| 185 | self.fill_order = fill_order |
|---|
| 186 | self.dx = dx; self.dy = dy; self.x0 = x0; self.y0 = y0; self.z = z |
|---|
| 187 | |
|---|
| 188 | def __eq__(self, other): |
|---|
| 189 | return reduce(and_, (getattr(self, attr) == getattr(other, attr) |
|---|
| 190 | for attr in ("aspect_ratio", "dx", "dy", |
|---|
| 191 | "x0", "y0", "fill_order"))) |
|---|
| 192 | |
|---|
| 193 | def calculate_size(self, n): |
|---|
| 194 | nx = math.sqrt(n*self.aspect_ratio) |
|---|
| 195 | ny = n/nx |
|---|
| 196 | return nx, ny |
|---|
| 197 | |
|---|
| 198 | def generate_positions(self, n): |
|---|
| 199 | nx, ny = self.calculate_size(n) |
|---|
| 200 | x,y,z = numpy.indices((nx,ny,1), dtype=float) |
|---|
| 201 | x = self.x0 + self.dx*x.flatten() |
|---|
| 202 | y = self.y0 + self.dy*y.flatten() |
|---|
| 203 | z = self.z + z.flatten() |
|---|
| 204 | if self.fill_order == 'sequential': |
|---|
| 205 | return numpy.array((x,y,z)) # use column_stack, if we decide to switch from (3,n) to (n,3) |
|---|
| 206 | else: |
|---|
| 207 | raise NotImplementedError |
|---|
| 208 | |
|---|
| 209 | def describe(self, n): |
|---|
| 210 | return "2D grid of size (%d, %d)" % self.calculate_size(n) |
|---|
| 211 | |
|---|
| 212 | |
|---|
| 213 | class Grid3D(BaseStructure): |
|---|
| 214 | |
|---|
| 215 | def __init__(self, aspect_ratioXY, aspect_ratioXZ, dx=1.0, dy=1.0, dz=1.0, x0=0.0, y0=0.0, z0=0, |
|---|
| 216 | fill_order="sequential"): |
|---|
| 217 | """ |
|---|
| 218 | If fill_order is 'sequential', the z-index will be filled first, then y then x, i.e. |
|---|
| 219 | the first cell will be at (0,0,0) (given default values for the other arguments), |
|---|
| 220 | the second at (0,0,1), etc. |
|---|
| 221 | """ |
|---|
| 222 | self.aspect_ratios = (aspect_ratioXY, aspect_ratioXZ) |
|---|
| 223 | assert fill_order in ('sequential', 'random') |
|---|
| 224 | self.fill_order = fill_order |
|---|
| 225 | self.dx = dx; self.dy = dy; self.dz = dz |
|---|
| 226 | self.x0 = x0; self.y0 = y0; self.z0 = z0 |
|---|
| 227 | |
|---|
| 228 | def __eq__(self, other): |
|---|
| 229 | return reduce(and_, (getattr(self, attr) == getattr(other, attr) |
|---|
| 230 | for attr in ("aspect_ratios", "dx", "dy", "dz", |
|---|
| 231 | "x0", "y0", "z0", "fill_order"))) |
|---|
| 232 | |
|---|
| 233 | def calculate_size(self, n): |
|---|
| 234 | a,b = self.aspect_ratios |
|---|
| 235 | nx = int(round(math.pow(n*a*b, 1/3.0))) |
|---|
| 236 | ny = int(round(nx/a)) |
|---|
| 237 | nz = int(round(nx/b)) |
|---|
| 238 | assert nx*ny*nz == n, str((nx, ny, nz, nx*ny*nz, n, a, b)) |
|---|
| 239 | return nx, ny, nz |
|---|
| 240 | |
|---|
| 241 | def generate_positions(self, n): |
|---|
| 242 | nx, ny, nz = self.calculate_size(n) |
|---|
| 243 | x,y,z = numpy.indices((nx,ny,nz), dtype=float) |
|---|
| 244 | x = self.x0 + self.dx*x.flatten() |
|---|
| 245 | y = self.y0 + self.dy*y.flatten() |
|---|
| 246 | z = self.z0 + self.dz*z.flatten() |
|---|
| 247 | if self.fill_order == 'sequential': |
|---|
| 248 | return numpy.array((x,y,z)) |
|---|
| 249 | else: |
|---|
| 250 | raise NotImplementedError |
|---|
| 251 | |
|---|
| 252 | def describe(self, n): |
|---|
| 253 | return "3D grid of size (%d, %d, %d)" % self.calculate_size(n) |
|---|
| 254 | |
|---|
| 255 | class Shape(object): |
|---|
| 256 | pass |
|---|
| 257 | |
|---|
| 258 | class Cuboid(Shape): |
|---|
| 259 | |
|---|
| 260 | def __init__(self, height, width, depth): |
|---|
| 261 | """Not sure how h,w,d should be related to x,y,z.""" |
|---|
| 262 | self.height = height |
|---|
| 263 | self.width = width |
|---|
| 264 | self.depth = depth |
|---|
| 265 | |
|---|
| 266 | def sample(self, n, rng): |
|---|
| 267 | return rng.uniform(size=(n,3)) * (self.height, self.width, self.depth) |
|---|
| 268 | |
|---|
| 269 | |
|---|
| 270 | class Sphere(Shape): |
|---|
| 271 | |
|---|
| 272 | def __init__(self, radius): |
|---|
| 273 | Shape.__init__(self) |
|---|
| 274 | self.radius = radius |
|---|
| 275 | |
|---|
| 276 | def sample(self, n, rng): |
|---|
| 277 | raise NotImplementedError |
|---|
| 278 | #theta_phi = rng.uniform(size=(n,2)) |
|---|
| 279 | #r = rng.??? |
|---|
| 280 | # now transform from r,theta,phi to x,y,z |
|---|
| 281 | |
|---|
| 282 | |
|---|
| 283 | class RandomStructure(BaseStructure): |
|---|
| 284 | |
|---|
| 285 | def __init__(self, boundary, origin=(0.0,0.0,0.0), rotation=(0,0), rng=None): |
|---|
| 286 | """ |
|---|
| 287 | `boundary` - a subclass of Shape |
|---|
| 288 | `origin` - a coordinate tuple (x,y,z) |
|---|
| 289 | `rotation` - (theta, phi) tuple giving the rotation to be applied (in degrees) |
|---|
| 290 | |
|---|
| 291 | Note that the rotation is applied before the origin shift |
|---|
| 292 | """ |
|---|
| 293 | self.boundary = boundary |
|---|
| 294 | self.origin = origin |
|---|
| 295 | self.rng = rng or NumpyRNG() |
|---|
| 296 | |
|---|
| 297 | def generate_positions(self, n): |
|---|
| 298 | return numpy.array(self.origin) + rotate(self.boundary.sample(n, rng), *self.origin) |
|---|
| 299 | |
|---|