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
0 commit comments