apsg 1.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,430 @@
1
+ import math
2
+ import numpy as np
3
+
4
+ from scipy.special import gamma as gamma_fun
5
+ from scipy.special import iv as modified_bessel_2ndkind
6
+ from scipy.special import ivp as modified_bessel_2ndkind_derivative
7
+ from scipy.stats import uniform
8
+ from scipy.stats import norm as gauss
9
+ from scipy.optimize import minimize_scalar
10
+
11
+
12
+ def vonMisesFisher(mu, kappa, num_samples):
13
+ """Generate N samples from von Mises Fisher
14
+ distribution around center mu in R^N with concentration kappa.
15
+
16
+ Adopted from https://github.com/jasonlaska/spherecluster
17
+ """
18
+
19
+ def _sample_weight(kappa):
20
+ """Rejection sampling scheme for sampling distance from center on
21
+ surface of the sphere.
22
+ """
23
+ b = 2 / (np.sqrt(4.0 * kappa**2 + 4) + 2 * kappa)
24
+ x = (1.0 - b) / (1.0 + b)
25
+ c = kappa * x + 2 * np.log(1 - x**2)
26
+
27
+ while True:
28
+ z = np.random.beta(1, 1)
29
+ w = (1.0 - (1.0 + b) * z) / (1.0 - (1.0 - b) * z)
30
+ u = np.random.uniform(low=0, high=1)
31
+ if kappa * w + 2 * np.log(1.0 - x * w) - c >= np.log(u):
32
+ return w
33
+
34
+ def _sample_orthonormal_to(mu):
35
+ """Sample point on sphere orthogonal to mu."""
36
+ v = np.random.randn(mu.shape[0])
37
+ proj_mu_v = mu * np.dot(mu, v) / np.linalg.norm(mu)
38
+ orthto = v - proj_mu_v
39
+ return orthto / np.linalg.norm(orthto)
40
+
41
+ result = np.zeros((num_samples, 3))
42
+ for nn in range(num_samples):
43
+ # sample offset from center (on sphere) with spread kappa
44
+ w = _sample_weight(kappa)
45
+
46
+ # sample a point v on the unit sphere that's orthogonal to mu
47
+ v = _sample_orthonormal_to(mu)
48
+
49
+ # compute new point
50
+ result[nn, :] = v * np.sqrt(1.0 - w**2) + w * mu
51
+
52
+ return result
53
+
54
+
55
+ def estimate_k(features):
56
+ # objective function to be minimized
57
+ def obj_fun(k):
58
+ W = np.exp(
59
+ k * (np.abs(np.dot(np.asarray(features), np.asarray(features).T)))
60
+ ) * (k / (4 * math.pi * math.sinh(k + 1e-9)))
61
+ np.fill_diagonal(W, 0.0)
62
+ return -np.log(W.sum(axis=0)).sum()
63
+
64
+ if len(features) > 1:
65
+ return minimize_scalar(obj_fun, bounds=(0.1, len(features)), method="bounded").x
66
+ else:
67
+ return 1
68
+
69
+
70
+ class KentDistribution(object):
71
+ """
72
+ The algorithms here are partially based on methods described in:
73
+ [The Fisher-Bingham Distribution on the Sphere, John T. Kent
74
+ Journal of the Royal Statistical Society. Series B (Methodological)
75
+ Vol. 44, No. 1 (1982), pp. 71-80 Published by: Wiley
76
+ Article Stable URL: http://www.jstor.org/stable/2984712]
77
+
78
+ Implementation by Daniel Fraenkel
79
+ https://github.com/edfraenkel/kent_distribution
80
+ """
81
+
82
+ minimum_value_for_kappa = 1e-6
83
+
84
+ @staticmethod
85
+ def create_matrix_H(theta, phi):
86
+ return np.array(
87
+ [
88
+ [np.cos(theta), -np.sin(theta), 0.0],
89
+ [
90
+ np.sin(theta) * np.cos(phi),
91
+ np.cos(theta) * np.cos(phi),
92
+ -np.sin(phi),
93
+ ],
94
+ [np.sin(theta) * np.sin(phi), np.cos(theta) * np.sin(phi), np.cos(phi)],
95
+ ]
96
+ )
97
+
98
+ @staticmethod
99
+ def create_matrix_Ht(theta, phi):
100
+ return np.transpose(KentDistribution.create_matrix_H(theta, phi))
101
+
102
+ @staticmethod
103
+ def create_matrix_K(psi):
104
+ return np.array(
105
+ [
106
+ [1.0, 0.0, 0.0],
107
+ [0.0, np.cos(psi), -np.sin(psi)],
108
+ [0.0, np.sin(psi), np.cos(psi)],
109
+ ]
110
+ )
111
+
112
+ @staticmethod
113
+ def create_matrix_Kt(psi):
114
+ return np.transpose(KentDistribution.create_matrix_K(psi))
115
+
116
+ @staticmethod
117
+ def create_matrix_Gamma(theta, phi, psi):
118
+ H = KentDistribution.create_matrix_H(theta, phi)
119
+ K = KentDistribution.create_matrix_K(psi)
120
+ return np.inner(H, np.transpose(K))
121
+
122
+ @staticmethod
123
+ def create_matrix_Gammat(theta, phi, psi):
124
+ return np.transpose(KentDistribution.create_matrix_Gamma(theta, phi, psi))
125
+
126
+ @staticmethod
127
+ def spherical_coordinates_to_gammas(theta, phi, psi):
128
+ Gamma = KentDistribution.create_matrix_Gamma(theta, phi, psi)
129
+ gamma1 = Gamma[:, 0]
130
+ gamma2 = Gamma[:, 1]
131
+ gamma3 = Gamma[:, 2]
132
+ return (gamma1, gamma2, gamma3)
133
+
134
+ @staticmethod
135
+ def gamma1_to_spherical_coordinates(gamma1):
136
+ theta = np.arccos(gamma1[0])
137
+ phi = np.arctan2(gamma1[2], gamma1[1])
138
+ return (theta, phi)
139
+
140
+ @staticmethod
141
+ def gammas_to_spherical_coordinates(gamma1, gamma2):
142
+ (theta, phi) = KentDistribution.gamma1_to_spherical_coordinates(gamma1)
143
+ Ht = KentDistribution.create_matrix_Ht(theta, phi)
144
+ u = np.inner(Ht, np.reshape(gamma2, (1, 3)))
145
+ psi = np.arctan2(u[2][0], u[1][0])
146
+ return (theta, phi, psi)
147
+
148
+ def __init__(self, gamma1, gamma2, gamma3, kappa, beta):
149
+ self.gamma1 = np.array(gamma1, dtype=np.float64)
150
+ self.gamma2 = np.array(gamma2, dtype=np.float64)
151
+ self.gamma3 = np.array(gamma3, dtype=np.float64)
152
+ self.kappa = float(kappa)
153
+ self.beta = float(beta)
154
+
155
+ (
156
+ self.theta,
157
+ self.phi,
158
+ self.psi,
159
+ ) = KentDistribution.gammas_to_spherical_coordinates(self.gamma1, self.gamma2)
160
+
161
+ for gamma in (gamma1, gamma2, gamma3):
162
+ assert len(gamma) == 3
163
+
164
+ self._cached_rvs = np.array([], dtype=np.float64)
165
+ self._cached_rvs.shape = (0, 3)
166
+
167
+ def __repr__(self):
168
+ return "kent(%s, %s, %s, %s, %s)" % (
169
+ self.theta,
170
+ self.phi,
171
+ self.psi,
172
+ self.kappa,
173
+ self.beta,
174
+ )
175
+
176
+ @property
177
+ def Gamma(self):
178
+ return self.create_matrix_Gamma(self.theta, self.phi, self.psi)
179
+
180
+ def normalize(self, cache=dict(), return_num_iterations=False):
181
+ """
182
+ Returns the normalization constant of the Kent distribution.
183
+ The proportional error may be expected not to be greater than
184
+ 1E-11.
185
+ """
186
+
187
+ (k, b) = (self.kappa, self.beta)
188
+ if (k, b) not in cache:
189
+ G = gamma_fun
190
+ Imb2 = modified_bessel_2ndkind
191
+ result = 0.0
192
+ j = 0
193
+ if b == 0.0:
194
+ result = (0.5 * k) ** (-2 * j - 0.5) * Imb2(2 * j + 0.5, k)
195
+ result /= G(j + 1)
196
+ result *= G(j + 0.5)
197
+ else:
198
+
199
+ while True:
200
+ a = np.exp(
201
+ np.log(b) * 2 * j + np.log(0.5 * k) * (-2 * j - 0.5)
202
+ ) * Imb2(2 * j + 0.5, k)
203
+ a /= G(j + 1)
204
+ a *= G(j + 0.5)
205
+ result += a
206
+
207
+ j += 1
208
+ if abs(a) < abs(result) * 1e-12 and j > 5:
209
+ break
210
+
211
+ cache[k, b] = 2 * np.pi * result
212
+ if return_num_iterations:
213
+ return (cache[k, b], j)
214
+ else:
215
+ return cache[k, b]
216
+
217
+ def log_normalize(self, return_num_iterations=False):
218
+ """
219
+ Returns the logarithm of the normalization constant.
220
+ """
221
+
222
+ if return_num_iterations:
223
+ (normalize, num_iter) = self.normalize(return_num_iterations=True)
224
+ return (np.log(normalize), num_iter)
225
+ else:
226
+ return np.log(self.normalize())
227
+
228
+ def pdf_max(self, normalize=True):
229
+ return np.exp(self.log_pdf_max(normalize))
230
+
231
+ def log_pdf_max(self, normalize=True):
232
+ """
233
+ Returns the maximum value of the log(pdf)
234
+ """
235
+
236
+ if self.beta == 0.0:
237
+ x = 1
238
+ else:
239
+ x = self.kappa * 1.0 / (2 * self.beta)
240
+ if x > 1.0:
241
+ x = 1
242
+ fmax = self.kappa * x + self.beta * (1 - x**2)
243
+ if normalize:
244
+ return fmax - self.log_normalize()
245
+ else:
246
+ return fmax
247
+
248
+ def pdf(self, xs, normalize=True):
249
+ """
250
+ Returns the pdf of the kent distribution for 3D vectors that
251
+ are stored in xs which must be an array of N x 3 or N x M x 3
252
+ N x M x P x 3 etc.
253
+ """
254
+
255
+ return np.exp(self.log_pdf(xs, normalize))
256
+
257
+ def log_pdf(self, xs, normalize=True):
258
+ """
259
+ Returns the log(pdf) of the kent distribution.
260
+ """
261
+
262
+ axis = len(np.shape(xs)) - 1
263
+ g1x = np.sum(self.gamma1 * xs, axis)
264
+ g2x = np.sum(self.gamma2 * xs, axis)
265
+ g3x = np.sum(self.gamma3 * xs, axis)
266
+ (k, b) = (self.kappa, self.beta)
267
+
268
+ f = k * g1x + b * (g2x**2 - g3x**2)
269
+ if normalize:
270
+ return f - self.log_normalize()
271
+ else:
272
+ return f
273
+
274
+ def pdf_prime(self, xs, normalize=True):
275
+ """
276
+ Returns the derivative of the pdf with respect to kappa and beta.
277
+ """
278
+
279
+ return self.pdf(xs, normalize) * self.log_pdf_prime(xs, normalize)
280
+
281
+ def log_pdf_prime(self, xs, normalize=True):
282
+ """
283
+ Returns the derivative of the log(pdf) with respect to kappa and beta.
284
+ """
285
+
286
+ axis = len(np.shape(xs)) - 1
287
+ g1x = np.sum(self.gamma1 * xs, axis)
288
+ g2x = np.sum(self.gamma2 * xs, axis)
289
+ g3x = np.sum(self.gamma3 * xs, axis)
290
+
291
+ dfdk = g1x
292
+ dfdb = g2x**2 - g3x**2
293
+ df = np.array([dfdk, dfdb])
294
+ if normalize:
295
+ return np.transpose(np.transpose(df) - self.log_normalize_prime())
296
+ else:
297
+ return df
298
+
299
+ def normalize_prime(self, cache=dict(), return_num_iterations=False):
300
+ """
301
+ Returns the derivative of the normalization factor with respect
302
+ to kappa and beta.
303
+ """
304
+
305
+ (k, b) = (self.kappa, self.beta)
306
+ if (k, b) not in cache:
307
+ G = gamma_fun
308
+ Imb2 = modified_bessel_2ndkind
309
+ (dcdk, dcdb) = (0.0, 0.0)
310
+ j = 0
311
+ if b == 0:
312
+ dcdk = (
313
+ G(j + 0.5)
314
+ / G(j + 1)
315
+ * ((-0.5 * j - 0.125) * k ** (-2 * j - 1.5))
316
+ * Imb2(2 * j + 0.5, k)
317
+ )
318
+ dcdk += (
319
+ G(j + 0.5)
320
+ / G(j + 1)
321
+ * (0.5 * k) ** (-2 * j - 0.5)
322
+ * modified_bessel_2ndkind_derivative(2 * j + 0.5, k, 1)
323
+ )
324
+
325
+ dcdb = 0.0
326
+ else:
327
+ while True:
328
+ dk = (
329
+ (-1 * j - 0.25)
330
+ * np.exp(np.log(b) * 2 * j + np.og(0.5 * k) * (-2 * j - 1.5))
331
+ * Imb2(2 * j + 0.5, k)
332
+ )
333
+ dk += np.exp(
334
+ np.log(b) * 2 * j + np.log(0.5 * k) * (-2 * j - 0.5)
335
+ ) * modified_bessel_2ndkind_derivative(2 * j + 0.5, k, 1)
336
+ dk /= G(j + 1)
337
+ dk *= G(j + 0.5)
338
+
339
+ db = (
340
+ 2
341
+ * j
342
+ * np.exp(
343
+ np.log(b) * (2 * j - 1) + np.log(0.5 * k) * (-2 * j - 0.5)
344
+ )
345
+ * Imb2(2 * j + 0.5, k)
346
+ )
347
+ db /= G(j + 1)
348
+ db *= G(j + 0.5)
349
+ dcdk += dk
350
+ dcdb += db
351
+
352
+ j += 1
353
+ if (
354
+ abs(dk) < abs(dcdk) * 1e-12
355
+ and abs(db) < abs(dcdb) * 1e-12
356
+ and j > 5
357
+ ):
358
+ break
359
+
360
+ # print("dc", dcdk, dcdb, "(", k, b)
361
+
362
+ cache[k, b] = 2 * np.pi * np.array([dcdk, dcdb])
363
+ if return_num_iterations:
364
+ return (cache[k, b], j)
365
+ else:
366
+ return cache[k, b]
367
+
368
+ def log_normalize_prime(self, return_num_iterations=False):
369
+ """
370
+ Returns the derivative of the logarithm of the normalization factor.
371
+ """
372
+
373
+ if return_num_iterations:
374
+ (normalize_prime, num_iter) = self.normalize_prime(
375
+ return_num_iterations=True
376
+ )
377
+ return (normalize_prime / self.normalize(), num_iter)
378
+ else:
379
+ return self.normalize_prime() / self.normalize()
380
+
381
+ def log_likelihood(self, xs):
382
+ """
383
+ Returns the log likelihood for xs.
384
+ """
385
+
386
+ retval = self.log_pdf(xs)
387
+ return np.sum(retval, len(np.shape(retval)) - 1)
388
+
389
+ def log_likelihood_prime(self, xs):
390
+ """
391
+ Returns the derivative with respect to kappa and beta of the log
392
+ likelihood for xs.
393
+ """
394
+
395
+ retval = self.log_pdf_prime(xs)
396
+ if len(np.shape(retval)) == 1:
397
+ return retval
398
+ else:
399
+ return np.sum(retval, len(np.shape(retval)) - 1)
400
+
401
+ def _rvs_helper(self):
402
+ num_samples = 10000
403
+ xs = gauss(0, 1).rvs((num_samples, 3))
404
+ xs = np.divide(xs, np.reshape(np.linalg.norm(xs, axis=1), (num_samples, 1)))
405
+ pvalues = self.pdf(xs, normalize=False)
406
+ fmax = self.pdf_max(normalize=False)
407
+ return xs[uniform(0, fmax).rvs(num_samples) < pvalues]
408
+
409
+ def rvs(self, n_samples=None):
410
+ """
411
+ Returns random samples from the Kent distribution by rejection sampling.
412
+ May become inefficient for large kappas.
413
+
414
+ The returned random samples are 3D unit vectors.
415
+ If n_samples == None then a single sample x is returned with shape (3,)
416
+ If n_samples is an integer value N then N samples are returned in an array with shape (N, 3)
417
+ """
418
+
419
+ num_samples = 1 if n_samples is None else n_samples
420
+ rvs = self._cached_rvs
421
+ while len(rvs) < num_samples:
422
+ new_rvs = self._rvs_helper()
423
+ rvs = np.concatenate([rvs, new_rvs])
424
+ if n_samples is None:
425
+ self._cached_rvs = rvs[1:]
426
+ return rvs[0]
427
+ else:
428
+ self._cached_rvs = rvs[num_samples:]
429
+ retval = rvs[:num_samples]
430
+ return retval