How to create random orthonormal matrix in python numpy How to create random orthonormal matrix in python numpy python python

How to create random orthonormal matrix in python numpy


Version 0.18 of scipy has scipy.stats.ortho_group and scipy.stats.special_ortho_group. The pull request where it was added is https://github.com/scipy/scipy/pull/5622

For example,

In [24]: from scipy.stats import ortho_group  # Requires version 0.18 of scipyIn [25]: m = ortho_group.rvs(dim=3)In [26]: mOut[26]: array([[-0.23939017,  0.58743526, -0.77305379],       [ 0.81921268, -0.30515101, -0.48556508],       [-0.52113619, -0.74953498, -0.40818426]])In [27]: np.set_printoptions(suppress=True)In [28]: m.dot(m.T)Out[28]: array([[ 1.,  0., -0.],       [ 0.,  1.,  0.],       [-0.,  0.,  1.]])


You can obtain a random n x n orthogonal matrix Q, (uniformly distributed over the manifold of n x n orthogonal matrices) by performing a QR factorization of an n x n matrix with elements i.i.d. Gaussian random variables of mean 0 and variance 1. Here is an example:

import numpy as npfrom scipy.linalg import qrn = 3H = np.random.randn(n, n)Q, R = qr(H)print (Q.dot(Q.T))
[[  1.00000000e+00  -2.77555756e-17   2.49800181e-16] [ -2.77555756e-17   1.00000000e+00  -1.38777878e-17] [  2.49800181e-16  -1.38777878e-17   1.00000000e+00]]

EDIT: (Revisiting this answer after the comment by @g g.) The claim above on the QR decomposition of a Gaussian matrix providing a uniformly distributed (over the, so called, Stiefel manifold) orthogonal matrix is suggested by Theorems 2.3.18-19 of this reference. Note that the statement of the result suggests a "QR-like" decomposition, however, with the triangular matrix R having positive elements.

Apparently, the qr function of scipy (numpy) function does not guarantee positive diagonal elements for R and the corresponding Q is actually not uniformly distributed. This has been observed in this monograph, Sec. 4.6 (the discussion refers to MATLAB, but I guess both MATLAB and scipy use the same LAPACK routines). It is suggested there that the matrix Q provided by qr is modified by post multiplying it with a random unitary diagonal matrix.

Below I reproduce the experiment in the above reference, plotting the empirical distribution (histogram) of phases of eigenvalues of the "direct" Q matrix provided by qr, as well as the "modified" version, where it is seen that the modified version does indeed have a uniform eigenvalue phase, as would be expected from a uniformly distributed orthogonal matrix.

from scipy.linalg import qr, eigvalsfrom seaborn import distplotn = 50repeats = 10000angles = []angles_modified = []for rp in range(repeats):    H = np.random.randn(n, n)    Q, R = qr(H)    angles.append(np.angle(eigvals(Q)))    Q_modified = Q @ np.diag(np.exp(1j * np.pi * 2 * np.random.rand(n)))    angles_modified.append(np.angle(eigvals(Q_modified))) fig, ax = plt.subplots(1,2, figsize = (10,3))distplot(np.asarray(angles).flatten(),kde = False, hist_kws=dict(edgecolor="k", linewidth=2), ax= ax[0])ax[0].set(xlabel='phase', title='direct')distplot(np.asarray(angles_modified).flatten(),kde = False, hist_kws=dict(edgecolor="k", linewidth=2), ax= ax[1])ax[1].set(xlabel='phase', title='modified');

enter image description here


This is the rvs method pulled from the https://github.com/scipy/scipy/pull/5622/files, with minimal change - just enough to run as a stand alone numpy function.

import numpy as np    def rvs(dim=3):     random_state = np.random     H = np.eye(dim)     D = np.ones((dim,))     for n in range(1, dim):         x = random_state.normal(size=(dim-n+1,))         D[n-1] = np.sign(x[0])         x[0] -= D[n-1]*np.sqrt((x*x).sum())         # Householder transformation         Hx = (np.eye(dim-n+1) - 2.*np.outer(x, x)/(x*x).sum())         mat = np.eye(dim)         mat[n-1:, n-1:] = Hx         H = np.dot(H, mat)         # Fix the last sign such that the determinant is 1     D[-1] = (-1)**(1-(dim % 2))*D.prod()     # Equivalent to np.dot(np.diag(D), H) but faster, apparently     H = (D*H.T).T     return H

It matches Warren's test, https://stackoverflow.com/a/38426572/901925