Coverage for /usr/local/lib/python3.10/site-packages/spam/orientations/generate.py: 100%

103 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-24 17:26 +0000

1import numpy 

2import scipy 

3import scipy.special 

4 

5 

6def generateIsotropic(N): 

7 """ 

8 There is no analytical solution for putting equally-spaced points on a unit sphere. 

9 This Saff and Kuijlaars spiral algorithm gets close. 

10 

11 Parameters 

12 ---------- 

13 N : integer 

14 Number of points to generate 

15 

16 Returns 

17 ------- 

18 orientations : Nx3 numpy array 

19 Z,Y,X unit vectors of orientations for each point on sphere 

20 

21 Note 

22 ---------- 

23 For references, see: 

24 http://www.cgafaq.info/wiki/Evenly_distributed_points_on_sphere 

25 

26 Which in turn was based on: 

27 http://sitemason.vanderbilt.edu/page/hmbADS 

28 

29 From: 

30 Rakhmanov, Saff and Zhou: **Minimal Discrete Energy on the Sphere**, Mathematical Research Letters, Vol. 1 (1994), pp. 647-662: 

31 https://www.math.vanderbilt.edu/~esaff/texts/155.pdf 

32 

33 Also see discussion here: 

34 http://groups.google.com/group/sci.math/browse_thread/thread/983105fb1ced42c/e803d9e3e9ba3d23#e803d9e3e9ba3d23%22%22 

35 """ 

36 

37 # Check that it is an integer 

38 assert isinstance(N, int), "\n spam.orientations.generateIsotropic: Number of vectors should be an integer" 

39 # Check value of number of vectors 

40 assert N > 0, "\n spam.orientations.generateIsotropic: Number of vectors should be > 0" 

41 

42 M = int(N) * 2 

43 

44 s = 3.6 / numpy.sqrt(M) 

45 

46 delta_z = 2 / float(M) 

47 z = 1 - delta_z / 2 

48 

49 longitude = 0 

50 

51 points = numpy.zeros((N, 3)) 

52 

53 for k in range(N): 

54 r = numpy.sqrt(1 - z * z) 

55 points[k, 2] = numpy.cos(longitude) * r 

56 points[k, 1] = numpy.sin(longitude) * r 

57 points[k, 0] = z 

58 z = z - delta_z 

59 longitude = longitude + s / r 

60 return points 

61 

62 

63def generateIcosphere(subDiv): 

64 """ 

65 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. 

66 The number of faces is 20*(4**subDiv). 

67 

68 Parameters 

69 ---------- 

70 subDiv : integer 

71 Number of times that the initial icosahedron is divided. 

72 Suggested value: 3 

73 

74 Returns 

75 ------- 

76 icoVerts: numberOfVerticesx3 numpy array 

77 Coordinates of the vertices of the icosphere 

78 

79 icoFaces: numberOfFacesx3 numpy array 

80 Indeces of the vertices that compose each face 

81 

82 icoVectors: numberOfFacesx3 

83 Vectors normal to each face 

84 

85 Note 

86 ---------- 

87 From: https://sinestesia.co/blog/tutorials/python-icospheres/ 

88 """ 

89 # Chech that it is an integer 

90 assert isinstance(subDiv, int), "\n spam.orientations.generateIcosphere: Number of subDiv should be an integer" 

91 assert subDiv > 0, print("\n spam.orientations.generateIcosphere: Number of subDiv should be > 0") 

92 

93 # 1. Internal functions 

94 

95 middle_point_cache = {} 

96 

97 def vertex(x, y, z): 

98 """Return vertex coordinates fixed to the unit sphere""" 

99 

100 length = numpy.sqrt(x**2 + y**2 + z**2) 

101 

102 return [i / length for i in (x, y, z)] 

103 

104 def middle_point(point_1, point_2): 

105 """Find a middle point and project to the unit sphere""" 

106 

107 # We check if we have already cut this edge first 

108 # to avoid duplicated verts 

109 smaller_index = min(point_1, point_2) 

110 greater_index = max(point_1, point_2) 

111 

112 key = "{}-{}".format(smaller_index, greater_index) 

113 

114 if key in middle_point_cache: 

115 return middle_point_cache[key] 

116 # If it's not in cache, then we can cut it 

117 vert_1 = icoVerts[point_1] 

118 vert_2 = icoVerts[point_2] 

119 middle = [sum(i) / 2 for i in zip(vert_1, vert_2)] 

120 icoVerts.append(vertex(middle[0], middle[1], middle[2])) 

121 index = len(icoVerts) - 1 

122 middle_point_cache[key] = index 

123 return index 

124 

125 # 2. Create the initial icosahedron 

126 # Golden ratio 

127 PHI = (1 + numpy.sqrt(5)) / 2 

128 icoVerts = [ 

129 vertex(-1, PHI, 0), 

130 vertex(1, PHI, 0), 

131 vertex(-1, -PHI, 0), 

132 vertex(1, -PHI, 0), 

133 vertex(0, -1, PHI), 

134 vertex(0, 1, PHI), 

135 vertex(0, -1, -PHI), 

136 vertex(0, 1, -PHI), 

137 vertex(PHI, 0, -1), 

138 vertex(PHI, 0, 1), 

139 vertex(-PHI, 0, -1), 

140 vertex(-PHI, 0, 1), 

141 ] 

142 

143 icoFaces = [ 

144 # 5 faces around point 0 

145 [0, 11, 5], 

146 [0, 5, 1], 

147 [0, 1, 7], 

148 [0, 7, 10], 

149 [0, 10, 11], 

150 # Adjacent faces 

151 [1, 5, 9], 

152 [5, 11, 4], 

153 [11, 10, 2], 

154 [10, 7, 6], 

155 [7, 1, 8], 

156 # 5 faces around 3 

157 [3, 9, 4], 

158 [3, 4, 2], 

159 [3, 2, 6], 

160 [3, 6, 8], 

161 [3, 8, 9], 

162 # Adjacent faces 

163 [4, 9, 5], 

164 [2, 4, 11], 

165 [6, 2, 10], 

166 [8, 6, 7], 

167 [9, 8, 1], 

168 ] 

169 

170 # 3. Work on the subdivisions 

171 for i in range(subDiv): 

172 faces_subDiv = [] 

173 for tri in icoFaces: 

174 v1 = middle_point(tri[0], tri[1]) 

175 v2 = middle_point(tri[1], tri[2]) 

176 v3 = middle_point(tri[2], tri[0]) 

177 faces_subDiv.append([tri[0], v1, v3]) 

178 faces_subDiv.append([tri[1], v2, v1]) 

179 faces_subDiv.append([tri[2], v3, v2]) 

180 faces_subDiv.append([v1, v2, v3]) 

181 icoFaces = faces_subDiv 

182 

183 # 4. Compute the normal vector to each face 

184 icoVectors = [] 

185 for tri in icoFaces: 

186 # Get the points 

187 P1 = numpy.array(icoVerts[tri[0]]) 

188 P2 = numpy.array(icoVerts[tri[1]]) 

189 P3 = numpy.array(icoVerts[tri[2]]) 

190 # Create two vector 

191 v1 = P2 - P1 

192 v2 = P2 - P3 

193 v3 = numpy.cross(v1, v2) 

194 norm = vertex(*v3) 

195 icoVectors.append(norm) 

196 

197 return icoVerts, icoFaces, icoVectors 

198 

199 

200def generateVonMisesFisher(mu, kappa, N=1): 

201 """ 

202 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. 

203 

204 Parameters 

205 ----------- 

206 mu : 1x3 array of floats 

207 Z, Y and X components of mean orientation. 

208 Non-unit vectors are normalised. 

209 

210 kappa : int 

211 Spread of the distribution, must be > 0. 

212 Higher values of kappa mean a higher concentration along the main orientation 

213 

214 N : int 

215 Number of vectors to generate 

216 

217 Returns 

218 -------- 

219 orientations : Nx3 array of floats 

220 Z, Y and X components of each vector. 

221 

222 Notes 

223 ----- 

224 Sampling method taken from https://github.com/dlwhittenbury/von-Mises-Fisher-Sampling 

225 

226 """ 

227 

228 def randUniformCircle(N): 

229 # N number of orientations 

230 v = numpy.random.normal(0, 1, (N, 2)) 

231 v = numpy.divide(v, numpy.linalg.norm(v, axis=1, keepdims=True)) 

232 return v 

233 

234 def randTmarginal(kappa, N=1): 

235 # Start of algorithm 

236 b = 2 / (2.0 * kappa + numpy.sqrt(4.0 * kappa**2 + 2**2)) 

237 x0 = (1.0 - b) / (1.0 + b) 

238 c = kappa * x0 + 2 * numpy.log(1.0 - x0**2) 

239 orientations = numpy.zeros((N, 1)) 

240 # Loop over number of orientations 

241 for i in range(N): 

242 # Continue unil you have an acceptable sample 

243 while True: 

244 # Sample Beta distribution 

245 Z = numpy.random.beta(1, 1) 

246 # Sample Uniform distributionNR 

247 U = numpy.random.uniform(low=0.0, high=1.0) 

248 # W is essentially t 

249 W = (1.0 - (1.0 + b) * Z) / (1.0 - (1.0 - b) * Z) 

250 # Check whether to accept or reject 

251 if kappa * W + 2 * numpy.log(1.0 - x0 * W) - c >= numpy.log(U): 

252 # Accept sample 

253 orientations[i] = W 

254 break 

255 return orientations 

256 

257 # Check for non-scalar value of kappa 

258 assert numpy.isscalar(kappa), "\n spam.orientations.generateVonMisesFisher: kappa should a scalar" 

259 assert kappa > 0, "\n spam.orientations.generateVonMisesFisher: kappa should be > 0" 

260 assert N > 1, "\n spam.orientations.generateVonMisesFisher: The number of vectors should be > 1" 

261 

262 try: 

263 mu = numpy.reshape(mu, (1, 3)) 

264 except Exception: 

265 print("\n spam.orientations.generateVonMisesFisher: The main orientation vector must be an array of 1x3") 

266 return 

267 # Normalize mu 

268 mu = mu / numpy.linalg.norm(mu) 

269 # check that N > 0! 

270 # Array to store orientations 

271 orientations = numpy.zeros((N, 3)) 

272 # Component in the direction of mu (Nx1) 

273 t = randTmarginal(kappa, N) 

274 # Component orthogonal to mu (Nx(p-1)) 

275 xi = randUniformCircle(N) 

276 # Component in the direction of mu (Nx1). 

277 orientations[:, [0]] = t 

278 # Component orthogonal to mu (Nx(p-1)) 

279 Eddy = numpy.sqrt(1 - t**2) 

280 orientations[:, 1:] = numpy.hstack((Eddy, Eddy)) * xi 

281 # Rotation of orientations to desired mu 

282 Olga = scipy.linalg.null_space(mu) 

283 Roubin = numpy.concatenate((mu.T, Olga), axis=1) 

284 orientations = numpy.dot(Roubin, orientations.T).T 

285 

286 return orientations