def log(self, x, y): k = self._k d, q = la.eigh(x) if k == 1: x_sqrt = [email protected](np.sqrt(d))@q.conj().T x_isqrt = [email protected](1/np.sqrt(d))@q.conj().T else: temp = np.zeros(q.shape, dtype=np.complex) for i in range(q.shape[0]): temp[i, :, :] = np.diag(np.sqrt(d[i, :]))[np.newaxis, :, :] x_sqrt = multiprod(multiprod(q, temp), multihconj(q)) temp = np.zeros(q.shape, dtype=np.complex) for i in range(q.shape[0]): temp[i, :, :] = np.diag(1/np.sqrt(d[i, :]))[np.newaxis, :, :] x_isqrt = multiprod(multiprod(q, temp), multihconj(q)) d, q = la.eigh(multiprod(multiprod(x_isqrt, y), x_isqrt)) if k == 1: log = [email protected](np.log(d))@q.conj().T else: temp = np.zeros(q.shape, dtype=np.complex) for i in range(q.shape[0]): temp[i, :, :] = np.diag(np.log(d[i, :]))[np.newaxis, :, :] d = temp log = multiprod(multiprod(q, d), multihconj(q)) xi = multiprod(multiprod(x_sqrt, log), x_sqrt) xi = multiherm(xi) return xi
def log(self, point_a, point_b): YHX = multihconj(point_b) @ point_a AH = multihconj(point_b) - YHX @ multihconj(point_a) BH = np.linalg.solve(YHX, AH) U, S, VH = np.linalg.svd(multihconj(BH), full_matrices=False) arctan_S = np.expand_dims(np.arctan(S), -2) return (U * arctan_S) @ VH
def exp(self, x, u): k = self._k d, q = la.eigh(x) if k == 1: x_sqrt = [email protected](np.sqrt(d))@q.conj().T x_isqrt = [email protected](1/np.sqrt(d))@q.conj().T else: temp = np.zeros(q.shape, dtype=np.complex) for i in range(q.shape[0]): temp[i, :, :] = np.diag(np.sqrt(d[i, :]))[np.newaxis, :, :] x_sqrt = multiprod(multiprod(q, temp), multihconj(q)) temp = np.zeros(q.shape, dtype=np.complex) for i in range(q.shape[0]): temp[i, :, :] = np.diag(1/np.sqrt(d[i, :]))[np.newaxis, :, :] x_isqrt = multiprod(multiprod(q, temp), multihconj(q)) d, q = la.eigh(multiprod(multiprod(x_isqrt, u), x_isqrt)) if k == 1: e = [email protected](np.exp(d))@q.conj().T else: temp = np.zeros(q.shape, dtype=np.complex) for i in range(q.shape[0]): temp[i, :, :] = np.diag(np.exp(d[i, :]))[np.newaxis, :, :] d = temp e = multiprod(multiprod(q, d), multihconj(q)) e = multiprod(multiprod(x_sqrt, e), x_sqrt) e = multiherm(e) return e
def log(self, X, Y): YHX = multiprod(multihconj(Y), X) AH = multihconj(Y) - multiprod(YHX, multihconj(X)) BH = np.linalg.solve(YHX, AH) U, S, VH = np.linalg.svd(multihconj(BH), full_matrices=False) arctan_S = np.expand_dims(np.arctan(S), -2) U = multiprod(U * arctan_S, VH) return U
def transp(self, x1, x2, d): E = multihconj(la.solve(multihconj(x1), multihconj(x2))) if self._k == 1: E = sqrtm(E) else: for i in range(len(E)): E[i, :, :] = sqrtm(E[i, :, :]) transp_d = multiprod(multiprod(E, d), multihconj(E)) return transp_d
def egrad(Q): """ need to be fixed """ QQ = np.matmul(Q, multihconj(Q)) tmp = np.array([QQ for i in range(N)]) XQQX = multiprod(multiprod(multihconj(X), tmp), X) lam, V = np.linalg.eigh(XQQX) theta = np.arccos(np.sqrt(lam)) d = -2*theta/(np.cos(theta)*np.sin(theta)) Sig = np.array([np.diag(dd) for dd in d]) XV = multiprod(X,V) eg = multiprod(XV, multiprod(Sig, multitransp(XV.conj()))) eg = np.mean(eg, axis = 0) eg = np.matmul(eg, Q) return eg
def rand(self): # Generate eigenvalues between 1 and 2 # (eigenvalues of a symmetric matrix are always real). d = np.ones((self._k, self._n, 1)) + rnd.rand(self._k, self._n, 1) # Generate an orthogonal matrix. Annoyingly qr decomp isn't # vectorized so need to use a for loop. Could be done using # svd but this is slower for bigger matrices. u = np.zeros((self._k, self._n, self._n), dtype=np.complex) for i in range(self._k): u[i], r = la.qr( rnd.randn(self._n, self._n)+1j*rnd.randn(self._n, self._n)) if self._k == 1: return multiprod(u, d * multihconj(u))[0] return multiprod(u, d * multihconj(u))
def test_multiexpm_conjugate_symmetric(self): shape = (self.k, self.m, self.m) A = np.random.normal(size=shape) + 1j * np.random.normal(size=shape) A = 0.5 * (A + multihconj(A)) # Compare fast path for conjugate symmetric matrices vs. general slow # one. np_testing.assert_allclose(multiexpm(A, symmetric=True), multiexpm(A, symmetric=False))
def norm(self, x, u): # This implementation is as fast as np.linalg.solve_triangular and is # more stable, as the above solver tends to output non positive # definite results. c = la.cholesky(x) c_inv = la.inv(c) return np.real( la.norm(multiprod(multiprod(c_inv, u), multihconj(c_inv))))
def test_rand(self): # Just make sure that things generated are on the manifold and that # if you generate two they are not equal. X = self.man.rand() np_testing.assert_allclose(multiprod(multihconj(X), X), multieye(self.k, self.n), atol=1e-10) Y = self.man.rand() assert la.norm(X - Y) > 1e-6 assert np.iscomplex(X).all()
def exp(self, point, tangent_vector): U, S, VH = np.linalg.svd(tangent_vector, full_matrices=False) cos_S = np.expand_dims(np.cos(S), -2) sin_S = np.expand_dims(np.sin(S), -2) Y = point @ (multihconj(VH) * cos_S) @ VH + (U * sin_S) @ VH # From numerical experiments, it seems necessary to # re-orthonormalize. This is overall quite expensive. q, _ = multiqr(Y) return q
def test_multilogm_complex_positive_definite(self): shape = (self.k, self.m, self.m) A = np.random.normal(size=shape) + 1j * np.random.normal(size=shape) A = A @ multihconj(A) # Compare fast path for positive definite matrices vs. general slow # one. np_testing.assert_allclose( multilogm(A, positive_definite=True), multilogm(A, positive_definite=False), )
def test_randvec(self): # Make sure things generated are in tangent space and if you generate # two then they are not equal. X = self.man.rand() U = self.man.randvec(X) np_testing.assert_allclose(multisym(multiprod(multihconj(X), U)), np.zeros((self.k, self.n, self.n)), atol=1e-10) V = self.man.randvec(X) assert la.norm(U - V) > 1e-6 assert np.iscomplex(U).all()
def test_random_point(self): # Just make sure that things generated are on the manifold # and that if you generate two they are not equal. # Test also that matrices are complex. X = self.manifold.random_point() np_testing.assert_allclose(multihconj(X) @ X, np.eye(self.n), atol=1e-10) Y = self.manifold.random_point() assert np.linalg.norm(X - Y) > 1e-6 assert np.iscomplex(X).all()
def test_proj(self): # Test proj(proj(X)) == proj(X) # and proj(X) belongs to the horizontal space of Stiefel X = self.man.rand() U = rnd.randn(self.m, self.n) + 1j*rnd.randn(self.m, self.n) proj_U = self.man.proj(X, U) proj_proj_U = self.man.proj(X, proj_U) np_testing.assert_allclose(proj_U, proj_proj_U) np_testing.assert_allclose(multiprod(multihconj(X), proj_U), np.zeros((self.n, self.n)), atol=1e-10)
def test_randvec(self): # Just make sure that things generated are on the horizontal space of # complex Stiefel manifold # and that if you generate two they are not equal. # Test also that matrices are complex. X = self.man.rand() G = self.man.randvec(X) np_testing.assert_allclose(multiprod(multihconj(X), G), np.zeros((self.n, self.n)), atol=1e-10) H = self.man.randvec(X) assert la.norm(G - H) > 1e-6 assert np.iscomplex(G).all()
def test_random_tangent_vector(self): # Make sure things generated are in tangent space and if you generate # two then they are not equal. X = self.manifold.random_point() U = self.manifold.random_tangent_vector(X) np_testing.assert_allclose( multisym(multihconj(X) @ U), np.zeros((self.k, self.n, self.n)), atol=1e-10, ) V = self.manifold.random_tangent_vector(X) assert np.linalg.norm(U - V) > 1e-6 assert np.iscomplex(U).all()
def test_retraction(self): # Test that the result is on the manifold and that for small # tangent vectors it has little effect. x = self.manifold.random_point() u = self.manifold.random_tangent_vector(x) xretru = self.manifold.retraction(x, u) np_testing.assert_allclose(multihconj(xretru) @ xretru, np.eye(self.n), atol=1e-10) u = u * 1e-6 xretru = self.manifold.retraction(x, u) np_testing.assert_allclose(xretru, x + u)
def test_retr(self): # Test that the result is on the manifold and that for small # tangent vectors it has little effect. x = self.man.rand() u = self.man.randvec(x) xretru = self.man.retr(x, u) np_testing.assert_allclose(multiprod(multihconj(xretru), xretru), np.eye(self.n), atol=1e-10) u = u * 1e-6 xretru = self.man.retr(x, u) np_testing.assert_allclose(xretru, x + u)
def test_projection(self): # Test proj(proj(X)) == proj(X) # and proj(X) belongs to the horizontal space of Stiefel X = self.manifold.random_point() U = np.random.normal(size=( self.m, self.n)) + 1j * np.random.normal(size=(self.m, self.n)) proj_U = self.manifold.projection(X, U) proj_proj_U = self.manifold.projection(X, proj_U) np_testing.assert_allclose(proj_U, proj_proj_U) np_testing.assert_allclose( multihconj(X) @ proj_U, np.zeros((self.n, self.n)), atol=1e-10, )
def exp(self, X, U): U, S, VH = np.linalg.svd(U, full_matrices=False) cos_S = np.expand_dims(np.cos(S), -2) sin_S = np.expand_dims(np.sin(S), -2) Y = (multiprod(multiprod(X, multihconj(VH) * cos_S), VH) + multiprod(U * sin_S, VH)) # From numerical experiments, it seems necessary to # re-orthonormalize. This is overall quite expensive. if self._k == 1: Y, _ = np.linalg.qr(Y) return Y else: for i in range(self._k): Y[i], _ = np.linalg.qr(Y[i]) return Y
def proj(self, X, U): return U - multiprod(X, multiprod(multihconj(X), U))
def dist(self, x, y): c = la.cholesky(x) c_inv = la.inv(c) logm = multilog(multiprod(multiprod(c_inv, y), multihconj(c_inv)), pos_def=True) return np.real(la.norm(logm))
def dist(self, X, Y): _, s, _ = np.linalg.svd(multiprod(multihconj(X), Y)) s[s > 1] = 1 s = np.arccos(s) return np.linalg.norm(np.real(s))
def ehess2rhess(self, X, egrad, ehess, H): PXehess = self.proj(X, ehess) XHG = multiprod(multihconj(X), egrad) HXHG = multiprod(H, XHG) return PXehess - HXHG
def DR_geod_complex(X, m, verbosity=0): """ X: array of N points on Gr(n, p); N x n x p array aim to represent X by X_hat (N points on Gr(m, p), m < n) where X_hat_i = R^T X_i, W \in St(n, m) minimizing the projection error (using geodesic distance) """ N, n, p = X.shape Cgr = ComplexGrassmann(n, p, N) Cgr_low = Grassmann(m, p) Cgr_map = ComplexGrassmann(n, m) # n x m XXT = multiprod(X, multihconj(X)) @pymanopt.function.Callable def cost(Q): tmp = np.array([np.matmul(Q, Q.T) for i in range(N)]) # N x n x n new_X = multiprod(tmp, X) # N x n x p q = np.array([qr(new_X[i])[0] for i in range(N)]) d2 = Cgr.dist(X, q)**2 return d2/N @pymanopt.function.Callable def egrad(Q): """ need to be fixed """ QQ = np.matmul(Q, multihconj(Q)) tmp = np.array([QQ for i in range(N)]) XQQX = multiprod(multiprod(multihconj(X), tmp), X) lam, V = np.linalg.eigh(XQQX) theta = np.arccos(np.sqrt(lam)) d = -2*theta/(np.cos(theta)*np.sin(theta)) Sig = np.array([np.diag(dd) for dd in d]) XV = multiprod(X,V) eg = multiprod(XV, multiprod(Sig, multitransp(XV.conj()))) eg = np.mean(eg, axis = 0) eg = np.matmul(eg, Q) return eg def egrad_num(R, eps = 1e-8+1e-8j): """ compute egrad numerically """ g = np.zeros(R.shape, dtype=np.complex128) for i in range(n): for j in range(m): R1 = R.copy() R2 = R.copy() R1[i,j] += eps R2[i,j] -= eps g[i,j] = (cost(R1) - cost(R2))/(2*eps) return g # solver = ConjugateGradient() solver = SteepestDescent() problem = Problem(manifold=Cst, cost=cost, egrad=egrad, verbosity=verbosity) Q_proj = solver.solve(problem) tmp = np.array([multihconj(Q_proj) for i in range(N)]) X_low = multiprod(tmp, X) X_low = X_low/np.expand_dims(np.linalg.norm(X_low, axis=1), axis = 2) M_hat = compute_centroid(Cgr_low, X_low) v_hat = var(Cgr_low, X_low, M_hat)/N var_ratio = v_hat/v return var_ratio, X_low, Q_proj
def euclidean_to_riemannian_hessian(self, point, euclidean_gradient, euclidean_hessian, tangent_vector): PXehess = self.projection(point, euclidean_hessian) XHG = multihconj(point) @ euclidean_gradient HXHG = tangent_vector @ XHG return PXehess - HXHG
def projection(self, point, vector): return vector - point @ multihconj(point) @ vector
def dist(self, point_a, point_b): s = np.linalg.svd(multihconj(point_a) @ point_b, compute_uv=False) s[s > 1] = 1 s = np.arccos(s) return np.linalg.norm(np.real(s))