Skip to content

Commit 6d51a2f

Browse files
committed
Added Gauss von Mises distribution
1 parent ad29e90 commit 6d51a2f

2 files changed

Lines changed: 156 additions & 0 deletions

File tree

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import numpy as np
2+
from scipy.special import iv
3+
from ..nonperiodic.gaussian_distribution import GaussianDistribution
4+
5+
6+
class GaussVMDistribution:
7+
def __init__(self, mu: np.ndarray, P: np.ndarray, alpha: float, beta: np.ndarray, Gamma: np.ndarray, kappa: float):
8+
# Check parameters
9+
assert mu.shape == (len(mu), 1), 'mu must be a column vector'
10+
assert P.shape == (len(mu), len(mu)), 'P and mu must have matching size'
11+
assert np.all(P == P.T), 'P must be symmetric'
12+
assert all(eigval > 0 for eigval in eigh(P, eigvals_only=True)), 'P must be positive definite'
13+
assert np.isscalar(alpha)
14+
assert beta.shape == mu.shape, 'size of beta must match size of mu'
15+
assert Gamma.shape == (len(mu), len(mu)), 'Gamma and mu must have matching size'
16+
assert np.all(Gamma == Gamma.T), 'Gamma must be symmetric'
17+
assert np.isscalar(kappa) and kappa > 0, 'kappa has to be a positive scalar'
18+
19+
# Assign parameters
20+
self.mu = mu
21+
self.P = P
22+
self.alpha = np.mod(alpha, 2 * np.pi)
23+
self.beta = beta
24+
self.Gamma = Gamma
25+
self.kappa = kappa
26+
self.A = np.linalg.cholesky(P)
27+
28+
self.linD = len(mu)
29+
self.boundD = 1
30+
self.dim = self.linD + self.boundD
31+
32+
def pdf(self, xa: np.ndarray) -> np.ndarray:
33+
assert xa.shape[0] == self.linD + 1
34+
p = multivariate_normal.pdf(xa[1:, :].T, mean=self.mu.ravel(), cov=self.P) * np.exp(
35+
self.kappa * np.cos(xa[0, :] - self.get_theta(xa[1:, :]))
36+
) / (2 * np.pi * besseli(0, self.kappa))
37+
return p
38+
39+
def get_theta(self, xa: np.ndarray) -> np.ndarray:
40+
z = np.linalg.solve(self.A, xa - np.tile(self.mu, (1, xa.shape[1])))
41+
theta = np.tile(self.alpha, (1, xa.shape[1])) + self.beta.T @ z + 0.5 * np.sum(
42+
(np.linalg.cholesky(self.Gamma) @ z) ** 2, axis=0
43+
)
44+
return theta
45+
46+
def mode(self) -> np.ndarray:
47+
return np.concatenate(([self.alpha], self.mu.ravel()))
48+
49+
def hybrid_moment(self) -> np.ndarray:
50+
M = np.eye(self.linD) - 1j * self.Gamma
51+
eiphi = 1 / np.sqrt(np.linalg.det(M)) * besselratio(0, self.kappa) * np.exp(
52+
1j * self.alpha - 0.5 * self.beta.T @ np.linalg.solve(M, self.beta)
53+
)
54+
mu = np.concatenate((np.real(eiphi), np.imag(eiphi), self.mu.ravel()))
55+
return mu
56+
57+
def sample_deterministic_horwood(self):
58+
# Horwood 5.1
59+
dim = self.linD
60+
61+
# solution for canonical form (mu = 0, P=I, alpha=beta=Gamma=0)
62+
B = lambda p, kappa: 1 - iv(p, kappa) / iv(0, kappa)
63+
xi = np.sqrt(3)
64+
eta = np.arccos(B(2, self.kappa) / 2 / B(1, self.kappa) - 1)
65+
wxi0 = 1 / 6
66+
weta0 = B(1, self.kappa)**2 / (4 * B(1, self.kappa) - B(2, self.kappa))
67+
w00 = 1 - 2 * weta0 - 2 * dim * wxi0
68+
N00 = np.zeros((dim + 1, 1))
69+
Neta0 = np.hstack((np.zeros((dim, 2)), np.array([[-eta, eta]]).T))
70+
Nxi0 = np.zeros((dim + 1, 2 * dim))
71+
for i in range(dim):
72+
Nxi0[i, 2 * i - 1] = -xi
73+
Nxi0[i, 2 * i] = xi
74+
75+
def to_gaussian(self):
76+
# Convert to Gaussian
77+
78+
# Uses approximation only valid for large kappa and small
79+
# Gamma, see Horwood, 4.6
80+
mtmp = np.hstack((self.alpha, self.mu))
81+
Atmp = np.vstack((np.hstack((1 / np.sqrt(self.kappa), self.beta.T)), np.hstack((np.zeros((self.linD, 1)), self.A))))
82+
Ptmp = Atmp @ Atmp.T
83+
gauss = GaussianDistribution(mtmp, Ptmp)
84+
return gauss
85+
86+
def linear_covariance(self):
87+
# Computes covariance of linear dimensions
88+
89+
# Returns:
90+
# C (linD x linD)
91+
# covariance matrix
92+
C = self.P
93+
return C
94+
95+
def marginalize_circular(self):
96+
gauss = GaussianDistribution(self.mu, self.P)
97+
return gauss
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import unittest
2+
import numpy as np
3+
from scipy.stats import multivariate_normal
4+
from scipy.special import iv
5+
6+
from pyrecest.distributions import GaussianDistribution
7+
from pyrecest.distributions.cart_prod.gauss_von_mises_distribution import GaussVonMisesDistribution
8+
9+
class GaussVonMisesDistributionTest(unittest.TestCase):
10+
11+
def setUp(self):
12+
self.g = GaussVonMisesDistribution(2, 1.3, 3, 0, 0.001, 0.7)
13+
self.testpoints = 2 * np.pi * np.random.rand(2, 100)
14+
np.random.seed(0) # equivalent to MATLAB's rng default
15+
16+
@staticmethod
17+
def _non_vectorized_pdf(gvm, xa):
18+
assert xa.shape[0] == gvm.mu.shape[0] + 1
19+
20+
if xa.shape[1] > 1:
21+
p = np.zeros((1, xa.shape[1]))
22+
for i in range(xa.shape[1]):
23+
p[0, i] = non_vectorized_pdf(gvm, xa[:, [i]])
24+
return p
25+
26+
angle = xa[0, :]
27+
z = np.linalg.solve(gvm.A, xa[1:, :] - gvm.mu)
28+
Theta = gvm.alpha + gvm.beta.T @ z + 0.5 * z.T @ gvm.Gamma @ z
29+
p = multivariate_normal.pdf(xa[1:, :].T, mean=gvm.mu.ravel(), cov=gvm.P) * np.exp(gvm.kappa * np.cos(angle - Theta)) / (2 * np.pi * iv(0, gvm.kappa))
30+
return p
31+
32+
def test_pdf(self):
33+
self.assertTrue(np.allclose(self.g.pdf(self.testpoints),
34+
GaussVonMisesDistributionTest._non_vectorized_pdf(self.g, self.testpoints), atol=1e-10))
35+
36+
def test_integral(self):
37+
self.assertAlmostEqual(self.g.integrate(), 1, delta=1e-5)
38+
39+
def test_mode(self):
40+
mode = self.g.mode()
41+
self.assertTrue(np.all(self.g.pdf(mode) >= self.g.pdf(self.testpoints)))
42+
43+
def test_to_gaussian(self):
44+
gauss = self.g.to_gaussian()
45+
self.assertIsInstance(gauss, GaussianDistribution)
46+
self.assertTrue(np.array_equal(gauss.mu, self.g.mode()))
47+
self.assertTrue(np.allclose(gauss.C[1:, 1:], self.g.P, atol=1e-10))
48+
49+
def test_sampling(self):
50+
n = 10
51+
s = self.g.sample_deterministic_horwood()
52+
self.assertEqual(s.shape, (self.g.lin_dim + self.g.bound_dim, n))
53+
54+
def test_hybrid_moment(self):
55+
hm = self.g.hybrid_moment()
56+
self.assertEqual(hm.shape, (self.g.lin_dim + 2 * self.g.bound_dim,))
57+
hmn = self.g.hybrid_moment_numerical()
58+
self.assertEqual(hmn.shape, (self.g.lin_dim + 2 * self.g.bound_dim,))
59+
self.assertTrue(np.allclose(hm, hmn, atol=1e-5))

0 commit comments

Comments
 (0)