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

103 statements  

« prev     ^ index     » next       coverage.py v7.2.3, created at 2023-11-22 13:26 +0000

1import math 

2 

3import numpy 

4import scipy 

5import scipy.special 

6 

7 

8def generateIsotropic(N): 

9 """ 

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

11 This Saff and Kuijlaars spiral algorithm gets close. 

12 

13 Parameters 

14 ---------- 

15 N : integer 

16 Number of points to generate 

17 

18 Returns 

19 ------- 

20 orientations : Nx3 numpy array 

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

22 

23 Note 

24 ---------- 

25 For references, see: 

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

27 

28 Which in turn was based on: 

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

30 

31 From: 

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

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

34 

35 Also see discussion here: 

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

37 """ 

38 

39 # Check that it is an integer 

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

41 # Check value of number of vectors 

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

43 

44 M = int(N) * 2 

45 

46 s = 3.6 / math.sqrt(M) 

47 

48 delta_z = 2 / float(M) 

49 z = 1 - delta_z / 2 

50 

51 longitude = 0 

52 

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

54 

55 for k in range(N): 

56 r = math.sqrt(1 - z * z) 

57 points[k, 2] = math.cos(longitude) * r 

58 points[k, 1] = math.sin(longitude) * r 

59 points[k, 0] = z 

60 z = z - delta_z 

61 longitude = longitude + s / r 

62 return points 

63 

64 

65def generateIcosphere(subDiv): 

66 """ 

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

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

69 

70 Parameters 

71 ---------- 

72 subDiv : integer 

73 Number of times that the initial icosahedron is divided. 

74 Suggested value: 3 

75 

76 Returns 

77 ------- 

78 icoVerts: numberOfVerticesx3 numpy array 

79 Coordinates of the vertices of the icosphere 

80 

81 icoFaces: numberOfFacesx3 numpy array 

82 Indeces of the vertices that compose each face 

83 

84 icoVectors: numberOfFacesx3 

85 Vectors normal to each face 

86 

87 Note 

88 ---------- 

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

90 """ 

91 # Chech that it is an integer 

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

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

94 

95 # 1. Internal functions 

96 

97 middle_point_cache = {} 

98 

99 def vertex(x, y, z): 

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

101 

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

103 

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

105 

106 def middle_point(point_1, point_2): 

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

108 

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

110 # to avoid duplicated verts 

111 smaller_index = min(point_1, point_2) 

112 greater_index = max(point_1, point_2) 

113 

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

115 

116 if key in middle_point_cache: 

117 return middle_point_cache[key] 

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

119 vert_1 = icoVerts[point_1] 

120 vert_2 = icoVerts[point_2] 

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

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

123 index = len(icoVerts) - 1 

124 middle_point_cache[key] = index 

125 return index 

126 

127 # 2. Create the initial icosahedron 

128 # Golden ratio 

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

130 icoVerts = [ 

131 vertex(-1, PHI, 0), 

132 vertex(1, PHI, 0), 

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

134 vertex(1, -PHI, 0), 

135 vertex(0, -1, PHI), 

136 vertex(0, 1, PHI), 

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

138 vertex(0, 1, -PHI), 

139 vertex(PHI, 0, -1), 

140 vertex(PHI, 0, 1), 

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

142 vertex(-PHI, 0, 1), 

143 ] 

144 

145 icoFaces = [ 

146 # 5 faces around point 0 

147 [0, 11, 5], 

148 [0, 5, 1], 

149 [0, 1, 7], 

150 [0, 7, 10], 

151 [0, 10, 11], 

152 # Adjacent faces 

153 [1, 5, 9], 

154 [5, 11, 4], 

155 [11, 10, 2], 

156 [10, 7, 6], 

157 [7, 1, 8], 

158 # 5 faces around 3 

159 [3, 9, 4], 

160 [3, 4, 2], 

161 [3, 2, 6], 

162 [3, 6, 8], 

163 [3, 8, 9], 

164 # Adjacent faces 

165 [4, 9, 5], 

166 [2, 4, 11], 

167 [6, 2, 10], 

168 [8, 6, 7], 

169 [9, 8, 1], 

170 ] 

171 

172 # 3. Work on the subdivisions 

173 for i in range(subDiv): 

174 faces_subDiv = [] 

175 for tri in icoFaces: 

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

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

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

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

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

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

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

183 icoFaces = faces_subDiv 

184 

185 # 4. Compute the normal vector to each face 

186 icoVectors = [] 

187 for tri in icoFaces: 

188 # Get the points 

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

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

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

192 # Create two vector 

193 v1 = P2 - P1 

194 v2 = P2 - P3 

195 v3 = numpy.cross(v1, v2) 

196 norm = vertex(*v3) 

197 icoVectors.append(norm) 

198 

199 return icoVerts, icoFaces, icoVectors 

200 

201 

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

203 """ 

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

205 

206 Parameters 

207 ----------- 

208 mu : 1x3 array of floats 

209 Z, Y and X components of mean orientation. 

210 Non-unit vectors are normalised. 

211 

212 kappa : int 

213 Spread of the distribution, must be > 0. 

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

215 

216 N : int 

217 Number of vectors to generate 

218 

219 Returns 

220 -------- 

221 orientations : Nx3 array of floats 

222 Z, Y and X components of each vector. 

223 

224 Notes 

225 ----- 

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

227 

228 """ 

229 

230 def randUniformCircle(N): 

231 # N number of orientations 

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

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

234 return v 

235 

236 def randTmarginal(kappa, N=1): 

237 # Start of algorithm 

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

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

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

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

242 # Loop over number of orientations 

243 for i in range(N): 

244 # Continue unil you have an acceptable sample 

245 while True: 

246 # Sample Beta distribution 

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

248 # Sample Uniform distributionNR 

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

250 # W is essentially t 

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

252 # Check whether to accept or reject 

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

254 # Accept sample 

255 orientations[i] = W 

256 break 

257 return orientations 

258 

259 # Check for non-scalar value of kappa 

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

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

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

263 

264 try: 

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

266 except Exception: 

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

268 return 

269 # Normalize mu 

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

271 # check that N > 0! 

272 # Array to store orientations 

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

274 # Component in the direction of mu (Nx1) 

275 t = randTmarginal(kappa, N) 

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

277 xi = randUniformCircle(N) 

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

279 orientations[:, [0]] = t 

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

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

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

283 # Rotation of orientations to desired mu 

284 Olga = scipy.linalg.null_space(mu) 

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

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

287 

288 return orientations