[docs]defgenerateIsotropic(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 integerassertisinstance(N,int),"\n spam.orientations.generateIsotropic: Number of vectors should be an integer"# Check value of number of vectorsassertN>0,"\n spam.orientations.generateIsotropic: Number of vectors should be > 0"M=int(N)*2s=3.6/math.sqrt(M)delta_z=2/float(M)z=1-delta_z/2longitude=0points=numpy.zeros((N,3))forkinrange(N):r=math.sqrt(1-z*z)points[k,2]=math.cos(longitude)*rpoints[k,1]=math.sin(longitude)*rpoints[k,0]=zz=z-delta_zlongitude=longitude+s/rreturnpoints
[docs]defgenerateIcosphere(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 integerassertisinstance(subDiv,int),"\n spam.orientations.generateIcosphere: Number of subDiv should be an integer"assertsubDiv>0,print("\n spam.orientations.generateIcosphere: Number of subDiv should be > 0")# 1. Internal functionsmiddle_point_cache={}defvertex(x,y,z):"""Return vertex coordinates fixed to the unit sphere"""length=numpy.sqrt(x**2+y**2+z**2)return[i/lengthforiin(x,y,z)]defmiddle_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 vertssmaller_index=min(point_1,point_2)greater_index=max(point_1,point_2)key="{}-{}".format(smaller_index,greater_index)ifkeyinmiddle_point_cache:returnmiddle_point_cache[key]# If it's not in cache, then we can cut itvert_1=icoVerts[point_1]vert_2=icoVerts[point_2]middle=[sum(i)/2foriinzip(vert_1,vert_2)]icoVerts.append(vertex(middle[0],middle[1],middle[2]))index=len(icoVerts)-1middle_point_cache[key]=indexreturnindex# 2. Create the initial icosahedron# Golden ratioPHI=(1+numpy.sqrt(5))/2icoVerts=[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 subdivisionsforiinrange(subDiv):faces_subDiv=[]fortriinicoFaces: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 faceicoVectors=[]fortriinicoFaces:# Get the pointsP1=numpy.array(icoVerts[tri[0]])P2=numpy.array(icoVerts[tri[1]])P3=numpy.array(icoVerts[tri[2]])# Create two vectorv1=P2-P1v2=P2-P3v3=numpy.cross(v1,v2)norm=vertex(*v3)icoVectors.append(norm)returnicoVerts,icoFaces,icoVectors
[docs]defgenerateVonMisesFisher(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 """defrandUniformCircle(N):# N number of orientationsv=numpy.random.normal(0,1,(N,2))v=numpy.divide(v,numpy.linalg.norm(v,axis=1,keepdims=True))returnvdefrandTmarginal(kappa,N=1):# Start of algorithmb=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 orientationsforiinrange(N):# Continue unil you have an acceptable samplewhileTrue:# Sample Beta distributionZ=numpy.random.beta(1,1)# Sample Uniform distributionNRU=numpy.random.uniform(low=0.0,high=1.0)# W is essentially tW=(1.0-(1.0+b)*Z)/(1.0-(1.0-b)*Z)# Check whether to accept or rejectifkappa*W+2*numpy.log(1.0-x0*W)-c>=numpy.log(U):# Accept sampleorientations[i]=Wbreakreturnorientations# Check for non-scalar value of kappaassertnumpy.isscalar(kappa),"\n spam.orientations.generateVonMisesFisher: kappa should a scalar"assertkappa>0,"\n spam.orientations.generateVonMisesFisher: kappa should be > 0"assertN>1,"\n spam.orientations.generateVonMisesFisher: The number of vectors should be > 1"try:mu=numpy.reshape(mu,(1,3))exceptException:print("\n spam.orientations.generateVonMisesFisher: The main orientation vector must be an array of 1x3")return# Normalize mumu=mu/numpy.linalg.norm(mu)# check that N > 0!# Array to store orientationsorientations=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 muOlga=scipy.linalg.null_space(mu)Roubin=numpy.concatenate((mu.T,Olga),axis=1)orientations=numpy.dot(Roubin,orientations.T).Treturnorientations