Source code for spam.orientations.generate

import math

import numpy
import scipy
import scipy.special


[docs] def generateIsotropic(N): """ There is no analytical solution for putting equally-spaced points on a unit sphere. This Saff and Kuijlaars spiral algorithm gets close. Parameters ---------- N : integer Number of points to generate Returns ------- orientations : Nx3 numpy array Z,Y,X unit vectors of orientations for each point on sphere Note ---------- For references, see: http://www.cgafaq.info/wiki/Evenly_distributed_points_on_sphere Which in turn was based on: http://sitemason.vanderbilt.edu/page/hmbADS From: Rakhmanov, Saff and Zhou: **Minimal Discrete Energy on the Sphere**, Mathematical Research Letters, Vol. 1 (1994), pp. 647-662: https://www.math.vanderbilt.edu/~esaff/texts/155.pdf Also see discussion here: http://groups.google.com/group/sci.math/browse_thread/thread/983105fb1ced42c/e803d9e3e9ba3d23#e803d9e3e9ba3d23%22%22 """ # Check that it is an integer assert isinstance(N, int), "\n spam.orientations.generateIsotropic: Number of vectors should be an integer" # Check value of number of vectors assert N > 0, "\n spam.orientations.generateIsotropic: Number of vectors should be > 0" M = int(N) * 2 s = 3.6 / math.sqrt(M) delta_z = 2 / float(M) z = 1 - delta_z / 2 longitude = 0 points = numpy.zeros((N, 3)) for k in range(N): r = math.sqrt(1 - z * z) points[k, 2] = math.cos(longitude) * r points[k, 1] = math.sin(longitude) * r points[k, 0] = z z = z - delta_z longitude = longitude + s / r return points
[docs] def generateIcosphere(subDiv): """ This function creates an unit icosphere (convex polyhedron made from triangles) starting from an icosahedron (polyhedron with 20 faces) and then making subdivision on each triangle. The number of faces is 20*(4**subDiv). Parameters ---------- subDiv : integer Number of times that the initial icosahedron is divided. Suggested value: 3 Returns ------- icoVerts: numberOfVerticesx3 numpy array Coordinates of the vertices of the icosphere icoFaces: numberOfFacesx3 numpy array Indeces of the vertices that compose each face icoVectors: numberOfFacesx3 Vectors normal to each face Note ---------- From: https://sinestesia.co/blog/tutorials/python-icospheres/ """ # Chech that it is an integer assert isinstance(subDiv, int), "\n spam.orientations.generateIcosphere: Number of subDiv should be an integer" assert subDiv > 0, print("\n spam.orientations.generateIcosphere: Number of subDiv should be > 0") # 1. Internal functions middle_point_cache = {} def vertex(x, y, z): """Return vertex coordinates fixed to the unit sphere""" length = numpy.sqrt(x**2 + y**2 + z**2) return [i / length for i in (x, y, z)] def middle_point(point_1, point_2): """Find a middle point and project to the unit sphere""" # We check if we have already cut this edge first # to avoid duplicated verts smaller_index = min(point_1, point_2) greater_index = max(point_1, point_2) key = "{}-{}".format(smaller_index, greater_index) if key in middle_point_cache: return middle_point_cache[key] # If it's not in cache, then we can cut it vert_1 = icoVerts[point_1] vert_2 = icoVerts[point_2] middle = [sum(i) / 2 for i in zip(vert_1, vert_2)] icoVerts.append(vertex(middle[0], middle[1], middle[2])) index = len(icoVerts) - 1 middle_point_cache[key] = index return index # 2. Create the initial icosahedron # Golden ratio PHI = (1 + numpy.sqrt(5)) / 2 icoVerts = [ vertex(-1, PHI, 0), vertex(1, PHI, 0), vertex(-1, -PHI, 0), vertex(1, -PHI, 0), vertex(0, -1, PHI), vertex(0, 1, PHI), vertex(0, -1, -PHI), vertex(0, 1, -PHI), vertex(PHI, 0, -1), vertex(PHI, 0, 1), vertex(-PHI, 0, -1), vertex(-PHI, 0, 1), ] icoFaces = [ # 5 faces around point 0 [0, 11, 5], [0, 5, 1], [0, 1, 7], [0, 7, 10], [0, 10, 11], # Adjacent faces [1, 5, 9], [5, 11, 4], [11, 10, 2], [10, 7, 6], [7, 1, 8], # 5 faces around 3 [3, 9, 4], [3, 4, 2], [3, 2, 6], [3, 6, 8], [3, 8, 9], # Adjacent faces [4, 9, 5], [2, 4, 11], [6, 2, 10], [8, 6, 7], [9, 8, 1], ] # 3. Work on the subdivisions for i in range(subDiv): faces_subDiv = [] for tri in icoFaces: v1 = middle_point(tri[0], tri[1]) v2 = middle_point(tri[1], tri[2]) v3 = middle_point(tri[2], tri[0]) faces_subDiv.append([tri[0], v1, v3]) faces_subDiv.append([tri[1], v2, v1]) faces_subDiv.append([tri[2], v3, v2]) faces_subDiv.append([v1, v2, v3]) icoFaces = faces_subDiv # 4. Compute the normal vector to each face icoVectors = [] for tri in icoFaces: # Get the points P1 = numpy.array(icoVerts[tri[0]]) P2 = numpy.array(icoVerts[tri[1]]) P3 = numpy.array(icoVerts[tri[2]]) # Create two vector v1 = P2 - P1 v2 = P2 - P3 v3 = numpy.cross(v1, v2) norm = vertex(*v3) icoVectors.append(norm) return icoVerts, icoFaces, icoVectors
[docs] def generateVonMisesFisher(mu, kappa, N=1): """ This function generates a set of N 3D unit vectors following a vonMises-Fisher distribution, centered at a mean orientation mu and with a spread K. Parameters ----------- mu : 1x3 array of floats Z, Y and X components of mean orientation. Non-unit vectors are normalised. kappa : int Spread of the distribution, must be > 0. Higher values of kappa mean a higher concentration along the main orientation N : int Number of vectors to generate Returns -------- orientations : Nx3 array of floats Z, Y and X components of each vector. Notes ----- Sampling method taken from https://github.com/dlwhittenbury/von-Mises-Fisher-Sampling """ def randUniformCircle(N): # N number of orientations v = numpy.random.normal(0, 1, (N, 2)) v = numpy.divide(v, numpy.linalg.norm(v, axis=1, keepdims=True)) return v def randTmarginal(kappa, N=1): # Start of algorithm b = 2 / (2.0 * kappa + numpy.sqrt(4.0 * kappa**2 + 2**2)) x0 = (1.0 - b) / (1.0 + b) c = kappa * x0 + 2 * numpy.log(1.0 - x0**2) orientations = numpy.zeros((N, 1)) # Loop over number of orientations for i in range(N): # Continue unil you have an acceptable sample while True: # Sample Beta distribution Z = numpy.random.beta(1, 1) # Sample Uniform distributionNR U = numpy.random.uniform(low=0.0, high=1.0) # W is essentially t W = (1.0 - (1.0 + b) * Z) / (1.0 - (1.0 - b) * Z) # Check whether to accept or reject if kappa * W + 2 * numpy.log(1.0 - x0 * W) - c >= numpy.log(U): # Accept sample orientations[i] = W break return orientations # Check for non-scalar value of kappa assert numpy.isscalar(kappa), "\n spam.orientations.generateVonMisesFisher: kappa should a scalar" assert kappa > 0, "\n spam.orientations.generateVonMisesFisher: kappa should be > 0" assert N > 1, "\n spam.orientations.generateVonMisesFisher: The number of vectors should be > 1" try: mu = numpy.reshape(mu, (1, 3)) except Exception: print("\n spam.orientations.generateVonMisesFisher: The main orientation vector must be an array of 1x3") return # Normalize mu mu = mu / numpy.linalg.norm(mu) # check that N > 0! # Array to store orientations orientations = numpy.zeros((N, 3)) # Component in the direction of mu (Nx1) t = randTmarginal(kappa, N) # Component orthogonal to mu (Nx(p-1)) xi = randUniformCircle(N) # Component in the direction of mu (Nx1). orientations[:, [0]] = t # Component orthogonal to mu (Nx(p-1)) Eddy = numpy.sqrt(1 - t**2) orientations[:, 1:] = numpy.hstack((Eddy, Eddy)) * xi # Rotation of orientations to desired mu Olga = scipy.linalg.null_space(mu) Roubin = numpy.concatenate((mu.T, Olga), axis=1) orientations = numpy.dot(Roubin, orientations.T).T return orientations