Commit b5881ee2 authored by maming's avatar maming
Browse files

Initial commit

parents
from lie_learn.representations.SO3.irrep_bases import *
from .spherical_harmonics import *
TEST_L_MAX = 5
def test_change_of_basis_matrix():
"""
Testing if change of basis matrix is consistent with spherical harmonics functions
"""
for l in range(TEST_L_MAX):
theta = np.random.rand() * np.pi
phi = np.random.rand() * np.pi * 2
for from_field in ['complex', 'real']:
for from_normalization in ['seismology', 'quantum', 'geodesy', 'nfft']:
for from_cs in ['cs', 'nocs']:
for to_field in ['complex', 'real']:
for to_normalization in ['seismology', 'quantum', 'geodesy', 'nfft']:
for to_cs in ['cs', 'nocs']:
Y_from = sh(l, np.arange(-l, l + 1), theta, phi,
from_field, from_normalization, from_cs == 'cs')
Y_to = sh(l, np.arange(-l, l + 1), theta, phi,
to_field, to_normalization, to_cs == 'cs')
B = change_of_basis_matrix(l=l,
frm=(from_field, from_normalization, 'centered', from_cs),
to=(to_field, to_normalization, 'centered', to_cs))
print(from_field, from_normalization, from_cs, '->', to_field, to_normalization, to_cs, np.sum(np.abs(B.dot(Y_from) - Y_to)))
assert np.isclose(np.sum(np.abs(B.dot(Y_from) - Y_to)), 0.0)
assert np.isclose(np.sum(np.abs(np.linalg.inv(B).dot(Y_to) - Y_from)), 0.0)
def test_change_of_basis_function():
"""
Testing if change of basis function is consistent with spherical harmonics functions
"""
for l in range(TEST_L_MAX):
theta = np.random.rand() * np.pi
phi = np.random.rand() * np.pi * 2
for from_field in ['complex', 'real']:
for from_normalization in ['seismology', 'quantum', 'geodesy', 'nfft']:
for from_cs in ['cs', 'nocs']:
for to_field in ['complex', 'real']:
for to_normalization in ['seismology', 'quantum', 'geodesy', 'nfft']:
for to_cs in ['cs', 'nocs']:
Y_from = sh(l, np.arange(-l, l + 1), theta, phi,
from_field, from_normalization, from_cs == 'cs')
Y_to = sh(l, np.arange(-l, l + 1), theta, phi,
to_field, to_normalization, to_cs == 'cs')
f = change_of_basis_function(l=l,
frm=(from_field, from_normalization, 'centered', from_cs),
to=(to_field, to_normalization, 'centered', to_cs))
print(from_field, from_normalization, from_cs, '->', to_field, to_normalization, to_cs, np.sum(np.abs(f(Y_from) - Y_to)))
assert np.isclose(np.sum(np.abs(f(Y_from) - Y_to)), 0.0)
def test_change_of_basis_function_lists():
"""
Testing change of basis function for spherical harmonics for multiple orders at once.
The change-of-basis function for spherical harmonics should be consistent with the CSH & RSH functions.
"""
l = np.arange(4)
ls = np.array([0, 1,1,1, 2,2,2,2,2, 3,3,3,3,3,3,3])
ms = np.array([0, -1,0,1, -2,-1,0,1,2, -3,-2,-1,0,1,2,3])
theta = np.random.rand() * np.pi
phi = np.random.rand() * np.pi * 2
for from_field in ['complex', 'real']:
for from_normalization in ['seismology', 'quantum', 'geodesy', 'nfft']:
for from_cs in ['cs', 'nocs']:
for to_field in ['complex', 'real']:
for to_normalization in ['seismology', 'quantum', 'geodesy', 'nfft']:
for to_cs in ['cs', 'nocs']:
Y_from = sh(ls, ms, theta, phi,
from_field, from_normalization, from_cs == 'cs')
Y_to = sh(ls, ms, theta, phi,
to_field, to_normalization, to_cs == 'cs')
f = change_of_basis_function(l=l,
frm=(from_field, from_normalization, 'centered', from_cs),
to=(to_field, to_normalization, 'centered', to_cs))
print(from_field, from_normalization, from_cs, '->', to_field, to_normalization, to_cs, np.sum(np.abs(f(Y_from) - Y_to)))
assert np.isclose(np.sum(np.abs(f(Y_from) - Y_to)), 0.0)
def test_invertibility():
"""
Testing if change_of_basis_function for SO(3) is invertible
"""
for l in range(TEST_L_MAX):
theta = np.random.rand() * np.pi
phi = np.random.rand() * np.pi * 2
for from_field in ['complex', 'real']:
for from_normalization in ['seismology', 'quantum', 'geodesy', 'nfft']:
for from_cs in ['cs', 'nocs']:
for from_order in ['centered', 'block']:
for to_field in ['complex', 'real']:
for to_normalization in ['seismology', 'quantum', 'geodesy', 'nfft']:
for to_cs in ['cs', 'nocs']:
for to_order in ['centered', 'block']:
# A truly complex function cannot be made real;
if from_field == 'complex' and to_field == 'real':
continue
if from_field == 'complex':
Y = np.random.randn(2 * l + 1) + np.random.randn(2 * l + 1) * 1j
else:
Y = np.random.randn(2 * l + 1)
f = change_of_basis_function(l=l,
frm=(from_field, from_normalization, from_order, from_cs),
to=(to_field, to_normalization, to_order, to_cs))
f_inv = change_of_basis_function(l=l,
frm=(to_field, to_normalization, to_order, to_cs),
to=(from_field, from_normalization, from_order, from_cs))
print(from_field, from_normalization, from_cs, from_order, '->', to_field, to_normalization, to_cs, to_order, np.sum(np.abs(f_inv(f(Y)) - Y)))
assert np.isclose(np.sum(np.abs(f_inv(f(Y)) - Y)), 0.)
#assert np.isclose(np.sum(np.abs(f(f_inv(Y)) - Y)), 0.)
def test_linearity_change_of_basis():
"""
Testing that SO3 change of basis is indeed linear
"""
for l in range(TEST_L_MAX):
theta = np.random.rand() * np.pi
phi = np.random.rand() * np.pi * 2
for from_field in ['complex', 'real']:
for from_normalization in ['seismology', 'quantum', 'geodesy', 'nfft']:
for from_cs in ['cs', 'nocs']:
for from_order in ['centered', 'block']:
for to_field in ['complex', 'real']:
for to_normalization in ['seismology', 'quantum', 'geodesy', 'nfft']:
for to_cs in ['cs', 'nocs']:
for to_order in ['centered', 'block']:
# A truly complex function cannot be made real;
if from_field == 'complex' and to_field == 'real':
continue
Y1 = np.random.randn(2 * l + 1)
Y2 = np.random.randn(2 * l + 1)
a = np.random.randn(1)
b = np.random.randn(1)
f = change_of_basis_function(l=l,
frm=(from_field, from_normalization, from_order, from_cs),
to=(to_field, to_normalization, from_order, to_cs))
print(from_field, from_normalization, from_cs, from_order, '->', to_field, to_normalization, to_cs, to_order, np.sum(np.abs(a * f(Y1) + b * f(Y2) - f(a*Y1 + b*Y2))))
assert np.isclose(np.sum(np.abs(a * f(Y1) + b * f(Y2) - f(a*Y1 + b*Y2))), 0.)
import numpy as np
import lie_learn.spaces.S2 as S2
from lie_learn.representations.SO3.spherical_harmonics import sh, sh_squared_norm
def check_orthogonality(L_max=3, grid_type='Gauss-Legendre',
field='real', normalization='quantum', condon_shortley=True):
theta, phi = S2.meshgrid(b=L_max + 1, grid_type=grid_type)
w = S2.quadrature_weights(b=L_max + 1, grid_type=grid_type)
for l in range(L_max):
for m in range(-l, l + 1):
for l2 in range(L_max):
for m2 in range(-l2, l2 + 1):
Ylm = sh(l, m, theta, phi, field, normalization, condon_shortley)
Ylm2 = sh(l2, m2, theta, phi, field, normalization, condon_shortley)
dot_numerical = S2.integrate_quad(Ylm * Ylm2.conj(), grid_type=grid_type, normalize=False, w=w)
dot_numerical2 = S2.integrate(
lambda t, p: sh(l, m, t, p, field, normalization, condon_shortley) * \
sh(l2, m2, t, p, field, normalization, condon_shortley).conj(), normalize=False)
sqnorm_analytical = sh_squared_norm(l, normalization, normalized_haar=False)
dot_analytical = sqnorm_analytical * (l == l2 and m == m2)
print(l, m, l2, m2, field, normalization, condon_shortley, dot_analytical, dot_numerical, dot_numerical2)
assert np.isclose(dot_numerical, dot_analytical)
assert np.isclose(dot_numerical2, dot_analytical)
def test_orthogonality():
L_max = 2
grid_type = 'Gauss-Legendre'
for field in ('real', 'complex'):
for normalization in ('quantum', 'seismology', 'geodesy', 'nfft'):
for condon_shortley in (True, False):
check_orthogonality(L_max, grid_type, field, normalization, condon_shortley)
import numpy as np
from lie_learn.representations.SO3.wigner_d import wigner_D_matrix, wigner_d_matrix,\
wigner_d_naive, wigner_d_naive_v2, wigner_d_naive_v3, wigner_d_function, wigner_D_function
import lie_learn.spaces.S3 as S3
TEST_L_MAX = 3
def check_unitarity_wigner_D():
"""
Check that the Wigner-D matrices are unitary.
We test every normalization convention and a range of input angles.
Note: only the quantum- or seismology normalized Wigner-D matrices are unitary,
so we do not check the geodesy and nfft normalized matrices.
"""
for l in range(TEST_L_MAX):
for field in ('real', 'complex'):
for normalization in ('quantum', 'seismology', 'geodesy', 'nfft'):
for order in ('centered', 'block'):
for condon_shortley in ('cs', 'nocs'):
for a in np.linspace(0, 2 * np.pi, 10):
for b in np.linspace(0, np.pi, 10):
for c in np.linspace(0, 2 * np.pi, 10):
m = wigner_D_matrix(l, a, b, c, field, normalization, order, condon_shortley)
diff = np.abs(m.conj().T.dot(m) - np.eye(m.shape[0])).sum()
diff += np.abs(m.dot(m.conj().T) - np.eye(m.shape[0])).sum()
print(l, field, normalization, order, condon_shortley, a, b, c, diff)
assert np.isclose(diff, 0.)
def check_normalization_wigner_D():
"""
According to [1], the Wigner D functions satisfy:
int_0^2pi da int_0^pi db sin(b) int_0^2pi |D^l_mn(a,b,c)|^2 = 8 pi^2 / (2l+1)
The factor 8 pi^2 is removed if we integrate with respect to the normalized Haar measure.
Here we test this equality by numerical integration.
NOTE: this test is subsumed in check_orthogonality_wigner_D, but that function is very slow
"""
w = S3.quadrature_weights(b=TEST_L_MAX + 1, grid_type='SOFT')
for l in range(TEST_L_MAX):
for m in range(-l, l + 1):
for n in range(-l, l + 1):
for field in ('real', 'complex'):
for normalization in ('quantum', 'seismology', 'geodesy', 'nfft'):
for order in ('centered', 'block'):
for condon_shortley in ('cs', 'nocs'):
f = lambda a, b, c: np.abs(wigner_D_function(
l=l, m=m, n=n, alpha=a, beta=b, gamma=c,
field=field, normalization=normalization,
order=order, condon_shortley=condon_shortley)) ** 2
sqnorm_numerical = S3.integrate(f, normalize=True)
D = make_D_sample_grid(b=TEST_L_MAX + 1, l=l, m=m, n=n,
field=field, normalization=normalization,
order=order, condon_shortley=condon_shortley)
sqnorm_numerical2 = S3.integrate_quad(D * D.conj(), grid_type='SOFT',
normalize=True, w=w)
sqnorm_analytical = 1. / (2 * l + 1)
print(l, m, n, field, normalization, order, condon_shortley, sqnorm_numerical, sqnorm_numerical2, sqnorm_analytical)
assert np.isclose(sqnorm_numerical, sqnorm_analytical)
assert np.isclose(sqnorm_numerical2, sqnorm_analytical)
def check_orthogonality_wigner_D():
"""
According to [1], the Wigner D functions satisfy:
int_0^2pi da int_0^pi db sin(b) int_0^2pi D^l_mn(a,b,c) D^l'_m'n'(a,b,c)*
=
8 pi^2 / (2l+1) delta(ll') delta(mm') delta(nn')
The factor 8 pi^2 is removed if we integrate with respect to the normalized Haar measure.
Here we test this equality by numerical integration.
"""
w = S3.quadrature_weights(b=TEST_L_MAX + 1, grid_type='SOFT')
for field in ('real', 'complex'):
for normalization in ('quantum', 'seismology', 'geodesy', 'nfft'):
for order in ('centered', 'block'):
for condon_shortley in ('cs', 'nocs'):
for l in range(TEST_L_MAX):
for m in range(-l, l + 1):
for n in range(-l, l + 1):
for l2 in range(TEST_L_MAX):
for m2 in range(-l2, l2 + 1):
for n2 in range(-l2, l2 + 1):
f = lambda a, b, c:\
wigner_D_function(
l=l, m=m, n=n, alpha=a, beta=b, gamma=c,
field=field, normalization=normalization,
order=order, condon_shortley=condon_shortley) * \
wigner_D_function(
l=l2, m=m2, n=n2, alpha=a, beta=b, gamma=c,
field=field, normalization=normalization,
order=order, condon_shortley=condon_shortley).conj()
D1 = make_D_sample_grid(b=TEST_L_MAX + 1, l=l, m=m, n=n,
field=field, normalization=normalization,
order=order, condon_shortley=condon_shortley)
D2 = make_D_sample_grid(b=TEST_L_MAX + 1, l=l2, m=m2, n=n2,
field=field, normalization=normalization,
order=order, condon_shortley=condon_shortley)
numerical_norm2 = S3.integrate_quad(D1 * D2.conj(), grid_type='SOFT',
normalize=True, w=w)
numerical_norm = S3.integrate(f, normalize=True)
analytical_norm = ((l == l2) * (m == m2) * (n == n2)) / (2 * l + 1)
print(field, normalization, order, condon_shortley, l, m, n, l2, m2, n2,
np.round(numerical_norm, 2),
np.round(numerical_norm2, 2),
np.round(analytical_norm, 2))
assert np.isclose(numerical_norm, analytical_norm)
assert np.isclose(numerical_norm2, analytical_norm)
def check_normalization_complex_wigner_d():
"""
According to [1], the following is true (eq. 12)
int_0^pi |d^l_mn(beta)|^2 sin(beta) dbeta = 1 / (2 l + 1)
NOTE: this function only tests the Wigner-d functions in the *complex basis*.
In this basis, the Wigner-d functions all have the same, simple norm: 2. / (2l + 1)
In the real basis, some functions are identically 0 and for the rest the norm is hard to understand.
We treat these in a separate function below.
[1] SOFT: SO(3) Fourier Transforms
Peter J. Kostelec and Daniel N. Rockmore
"""
# The squared L2 norm
# By squared L2 norm of f we mean |f|^2 = int_SO(3) |f(g)|^2 dg, where dg is the normalized Haar measure
L2_norm = lambda l: 2. / (2 * l + 1) # Note the factor 2..
for l in range(TEST_L_MAX):
for m in range(-l, l + 1):
for n in range(-l, l + 1):
for field in ('complex',): # Only test complex d functions here
for normalization in ('quantum', 'seismology', 'geodesy', 'nfft'):
for condon_shortley in ('cs', 'nocs'):
for order in ('centered', 'block'):
f = lambda b: wigner_d_matrix(
l=l, beta=b,
field=field, normalization=normalization,
order=order, condon_shortley=condon_shortley)[l + m, l + n] ** 2 * np.sin(b)
# from scipy.integrate import quad
# res = quad(f, a=0, b=np.pi, full_output=1)
# val = res[0]
# if not np.isclose(val, L2_norm[normalization](l)):
# print(res)
val = myquad(f, 0, np.pi)
print(l, m, n, field, normalization, order, condon_shortley,
np.round(val, 2),
np.round(L2_norm(l), 2))
assert np.isclose(val, L2_norm(l))
def check_orthogonality_complex_wigner_d():
"""
According to [1], the following is true (eq. 12)
int_0^pi d^l_mn(beta) d^l'_mn(beta) sin(beta) dbeta = 1 / (2 l + 1) delta(l,l')
NOTE: this function only tests the Wigner-d functions in the *complex basis*.
In this basis, the Wigner-d functions all have the same, simple norm: 2. / (2l + 1)
In the real basis, some functions are identically 0 and for the rest the norm is hard to understand.
We treat these in a separate function below.
NOTE: we only test in centered, not the block basis. For some reason this equality fails in the block basis.
I have not investigated the reason for this yet.
[1] SOFT: SO(3) Fourier Transforms
Peter J. Kostelec and Daniel N. Rockmore
:return:
"""
# The squared L2 norm for each of the normalizations
# By squared L2 norm of f we mean |f|^2 = int_SO(3) |f(g)|^2 dg, where dg is the normalized Haar measure
L2_norm = lambda l: 2. / (2 * l + 1)
for field in ('complex',):
for normalization in ('quantum', 'seismology', 'geodesy', 'nfft'):
for condon_shortley in ('cs', 'nocs'):
for order in ('centered',): # 'block'):
for m in range(-TEST_L_MAX, TEST_L_MAX + 1):
for n in range(-TEST_L_MAX, TEST_L_MAX + 1):
for l in range(np.maximum(np.abs(m), np.abs(n)), TEST_L_MAX):
for l2 in range(np.maximum(np.abs(m), np.abs(n)), TEST_L_MAX):
f = lambda b:\
wigner_d_function(
l=l, m=m, n=n, beta=b,
field=field, normalization=normalization,
order=order, condon_shortley=condon_shortley) * \
wigner_d_function(
l= l2, m=m, n=n, beta=b,
field=field, normalization=normalization,
order=order, condon_shortley=condon_shortley) * \
np.sin(b)
# from scipy.integrate import quad
# res = quad(f, a=0, b=np.pi, full_output=1)
# val = res[0]
# if not np.isclose(val, L2_norm[normalization](l)):
# print(res)
numerical_inner_product = myquad(f, 0, np.pi)
analytical_inner_product = L2_norm(l) * (l == l2)
print(l, l2, m, n, field, normalization, order, condon_shortley,
np.round(numerical_inner_product, 2),
np.round(analytical_inner_product, 2))
assert np.isclose(numerical_inner_product, analytical_inner_product,
rtol=1e-4, atol=1e-5)
def check_orthogonality_naive_wigner_d():
"""
According to [1], the following is true (eq. 12)
int_0^pi d^l_mn(beta) d^l'_mn(beta) sin(beta) dbeta = 1 / (2 l + 1) delta(l, l')
Here we check this equality numerically for the *naive* implementations of the Wigner-d functions
[1] SOFT: SO(3) Fourier Transforms
Peter J. Kostelec and Daniel N. Rockmore
:return:
"""
# The squared L2 norm for each of the normalizations
# By squared L2 norm of f we mean |f|^2 = int_SO(3) |f(g)|^2 dg, where dg is the normalized Haar measure
L2_norm = lambda l: 2. / (2 * l + 1)
for m in range(-TEST_L_MAX, TEST_L_MAX + 1):
for n in range(-TEST_L_MAX, TEST_L_MAX + 1):
for l in range(np.maximum(np.abs(m), np.abs(n)), TEST_L_MAX):
for l2 in range(np.maximum(np.abs(m), np.abs(n)), TEST_L_MAX):
f1 = lambda b: \
wigner_d_naive(l=l, m=m, n=n, beta=b) * \
wigner_d_naive(l=l2, m=m, n=n, beta=b) * \
np.sin(b)
f2 = lambda b: \
wigner_d_naive_v2(l=l, m=m, n=n, beta=b) * \
wigner_d_naive_v2(l=l2, m=m, n=n, beta=b) * \
np.sin(b)
f3 = lambda b: \
wigner_d_naive_v3(l=l, m=m, n=n)(b) * \
wigner_d_naive_v3(l=l2, m=m, n=n)(b) * \
np.sin(b)
for f in (f1, f2, f3):
# from scipy.integrate import quad
# res = quad(f, a=0, b=np.pi, full_output=1)
# val = res[0]
# if not np.isclose(val, L2_norm[normalization](l)):
# print(res)
numerical_inner_product = myquad(f, 0, np.pi)
analytical_inner_product = L2_norm(l) * (l == l2)
print(l, l2, m, n,
np.round(numerical_inner_product, 2),
np.round(analytical_inner_product, 2))
assert np.isclose(numerical_inner_product, analytical_inner_product,
rtol=1e-4, atol=1e-5)
def myquad(f, a, b):
n = 1000
v = 0.
for x in np.linspace(a, b, num=n, endpoint=False):
v += f(x)
return v * (b - a) / n
# TODO: this test is failing - I'm not sure what the norms for real Wigner-d functions should be (see comments below)
def check_normalization_wigner_d_real(L_max=TEST_L_MAX):
"""
According to [1], the following is true (eq. 12)
int_0^pi d^l_mn(beta) d^l'_mn(beta) sin(beta) dbeta = 1 / (2 l + 1) delta(l, l')
[1] SOFT: SO(3) Fourier Transforms
Peter J. Kostelec and Daniel N. Rockmore
:return:
"""
# Note: this function is called "check" not "test" because this function is expensive to evaluate and we don't
# want to automatically call this when running nosetests.
# The squared L2 norm for each of the normalizations
# By L2 norm of f we mean int_SO(3) |f(g)|^2 dg, where dg is the normalized Haar measure
L2_norm = {
'quantum': lambda l: 2. / (2 * l + 1),
'seismology': lambda l: 2. / (2 * l + 1),
'geodesy': lambda l: 2. / (2 * l + 1),
'nfft': lambda l: 2. / (2 * l + 1)
}
correct = [np.zeros((2 * l + 1, 2 * l + 1)) for l in range(L_max)]
ratio = [np.zeros((2 * l + 1, 2 * l + 1)) for l in range(L_max)]
vals = [np.zeros((2 * l + 1, 2 * l + 1)) for l in range(L_max)]
# Note: this seems to be correct for complex wigners in all normalizations, orders, cs, l, m, n,
# For the real ones, we can understand which wigners are identically zero,
# the norms for the non-zeros appear to be pretty complicated. Plotting the norms for l=9, we see a band pattern
# similar to the appearance of the wigner-d matrix itself.
# This matrix is symmetric, so the norm for dmn equals the norm for dnm
# See note above in check_normalization_wigner_d_complex
# The norm of the real wigner-d functions seems to be hard to understand. The norm now depends on m,n as well as l
# We can understand which real wigners are identically zero (see real_zeros below, or plot a d-matrix).
# Plotting the norms for l=9, we see a moire-like pattern for the non-zero wigners,
# similar in appearance to the wigner-d matrix itself.
# This matrix is symmetric, so the norm for dmn equals the norm for dnm
for order in ('centered',): # 'block'):
for l in range(L_max):
for m in range(-l, l + 1):
for n in range(-l, l + 1):
for field in ('real',): # only test real here
for normalization in ('quantum',): # 'seismology', 'geodesy', 'nfft'): all normalization seem to give the same behaviour
for condon_shortley in ('cs',): # 'nocs'): doesn't seem to matter
f = lambda b: wigner_d_matrix(
l=l, beta=b,
field=field, normalization=normalization,
order=order, condon_shortley=condon_shortley)[l + m, l + n] ** 2 * np.sin(b)
# from scipy.integrate import quad
# res = quad(f, a=0, b=np.pi, full_output=1)
# val = res[0]
# if not np.isclose(val, L2_norm[normalization](l)):
# print(res)
val = myquad(f, 0, np.pi)
real_zeros = ((m < 0 and n >= 0) or (m >= 0 and n < 0)) and field == 'real'
print(l, m, n, # field, normalization, order, condon_shortley,
np.round(val, 2),
np.round(L2_norm[normalization](l) * (not real_zeros), 2))
# assert np.isclose(val, L2_norm[normalization](l))
# if not np.isclose(val, L2_norm[normalization](l)):
# print("!!!!!")
correct[l][l + m, l + n] = np.isclose(val, L2_norm[normalization](l) * (not real_zeros))
ratio[l][l + m, l + n] = val / (L2_norm[normalization](l) * (not real_zeros)) if (not real_zeros) else 1
vals[l][l + m, l + n] = val
return correct, ratio, vals
def make_D_sample_grid(b=4, l=0, m=0, n=0,
field='complex', normalization='seismology', order='centered', condon_shortley='cs'):
from lie_learn.representations.SO3.wigner_d import wigner_D_function
D = lambda a, b, c: wigner_D_function(l, m, n, alpha, beta, gamma,
field=field, normalization=normalization,
order=order, condon_shortley=condon_shortley)
f = np.zeros((2 * b, 2 * b, 2 * b), dtype=complex if field == 'complex' else float)
for j1 in range(f.shape[0]):
alpha = 2 * np.pi * j1 / (2. * b)
for k in range(f.shape[1]):
beta = np.pi * (2 * k + 1) / (4. * b)
for j2 in range(f.shape[2]):
gamma = 2 * np.pi * j2 / (2. * b)
f[j1, k, j2] = D(alpha, beta, gamma)
return f
import numpy as np
from lie_learn.representations.SO3.pinchon_hoggan.pinchon_hoggan_dense import Jd, rot_mat
from lie_learn.representations.SO3.irrep_bases import change_of_basis_matrix
def wigner_d_matrix(l, beta,
field='real', normalization='quantum', order='centered', condon_shortley='cs'):
"""
Compute the Wigner-d matrix of degree l at beta, in the basis defined by
(field, normalization, order, condon_shortley)
The Wigner-d matrix of degree l has shape (2l + 1) x (2l + 1).
:param l: the degree of the Wigner-d function. l >= 0
:param beta: the argument. 0 <= beta <= pi
:param field: 'real' or 'complex'
:param normalization: 'quantum', 'seismology', 'geodesy' or 'nfft'
:param order: 'centered' or 'block'
:param condon_shortley: 'cs' or 'nocs'
:return: d^l_mn(beta) in the chosen basis
"""
# This returns the d matrix in the (real, quantum-normalized, centered, cs) convention
d = rot_mat(alpha=0., beta=beta, gamma=0., l=l, J=Jd[l])
if (field, normalization, order, condon_shortley) != ('real', 'quantum', 'centered', 'cs'):
# TODO use change of basis function instead of matrix?
B = change_of_basis_matrix(
l,
frm=('real', 'quantum', 'centered', 'cs'),
to=(field, normalization, order, condon_shortley))
BB = change_of_basis_matrix(
l,
frm=(field, normalization, order, condon_shortley),
to=('real', 'quantum', 'centered', 'cs'))
d = B.dot(d).dot(BB)
# The Wigner-d matrices are always real, even in the complex basis
# (I tested this numerically, and have seen it in several texts)
# assert np.isclose(np.sum(np.abs(d.imag)), 0.0)
d = d.real
return d
def wigner_D_matrix(l, alpha, beta, gamma,
field='real', normalization='quantum', order='centered', condon_shortley='cs'):
"""
Evaluate the Wigner-d matrix D^l_mn(alpha, beta, gamma)
:param l: the degree of the Wigner-d function. l >= 0
:param alpha: the argument. 0 <= alpha <= 2 pi
:param beta: the argument. 0 <= beta <= pi
:param gamma: the argument. 0 <= gamma <= 2 pi
:param field: 'real' or 'complex'
:param normalization: 'quantum', 'seismology', 'geodesy' or 'nfft'
:param order: 'centered' or 'block'
:param condon_shortley: 'cs' or 'nocs'
:return: D^l_mn(alpha, beta, gamma) in the chosen basis
"""
D = rot_mat(alpha=alpha, beta=beta, gamma=gamma, l=l, J=Jd[l])
if (field, normalization, order, condon_shortley) != ('real', 'quantum', 'centered', 'cs'):
B = change_of_basis_matrix(
l,
frm=('real', 'quantum', 'centered', 'cs'),
to=(field, normalization, order, condon_shortley))
BB = change_of_basis_matrix(
l,
frm=(field, normalization, order, condon_shortley),
to=('real', 'quantum', 'centered', 'cs'))
D = B.dot(D).dot(BB)
if field == 'real':
# print('WIGNER D IMAG PART:', np.sum(np.abs(D.imag)))
assert np.isclose(np.sum(np.abs(D.imag)), 0.0)
D = D.real
return D
def wigner_d_function(l, m, n, beta,
field='real', normalization='quantum', order='centered', condon_shortley='cs'):
"""
Evaluate a single Wigner-d function d^l_mn(beta)
NOTE: for now, we implement this by computing the entire degree-l Wigner-d matrix and then selecting
the (m,n) element, so this function is not fast.
:param l: the degree of the Wigner-d function. l >= 0
:param m: the order of the Wigner-d function. -l <= m <= l
:param n: the order of the Wigner-d function. -l <= n <= l
:param beta: the argument. 0 <= beta <= pi
:param field: 'real' or 'complex'
:param normalization: 'quantum', 'seismology', 'geodesy' or 'nfft'
:param order: 'centered' or 'block'
:param condon_shortley: 'cs' or 'nocs'
:return: d^l_mn(beta) in the chosen basis
"""
return wigner_d_matrix(l, beta, field, normalization, order, condon_shortley)[l + m, l + n]
def wigner_D_function(l, m, n, alpha, beta, gamma,
field='real', normalization='quantum', order='centered', condon_shortley='cs'):
"""
Evaluate a single Wigner-d function d^l_mn(beta)
NOTE: for now, we implement this by computing the entire degree-l Wigner-D matrix and then selecting
the (m,n) element, so this function is not fast.
:param l: the degree of the Wigner-d function. l >= 0
:param m: the order of the Wigner-d function. -l <= m <= l
:param n: the order of the Wigner-d function. -l <= n <= l
:param alpha: the argument. 0 <= alpha <= 2 pi
:param beta: the argument. 0 <= beta <= pi
:param gamma: the argument. 0 <= gamma <= 2 pi
:param field: 'real' or 'complex'
:param normalization: 'quantum', 'seismology', 'geodesy' or 'nfft'
:param order: 'centered' or 'block'
:param condon_shortley: 'cs' or 'nocs'
:return: d^l_mn(beta) in the chosen basis
"""
return wigner_D_matrix(l, alpha, beta, gamma, field, normalization, order, condon_shortley)[l + m, l + n]
def wigner_D_norm(l, normalized_haar=True):
"""
Compute the squared norm of the Wigner-D functions.
The squared norm of a function on the SO(3) is defined as
|f|^2 = int_SO(3) |f(g)|^2 dg
where dg is a Haar measure.
:param l: for some normalization conventions, the norm of a Wigner-D function D^l_mn depends on the degree l
:param normalized_haar: whether to use the Haar measure da db sinb dc or the normalized Haar measure
da db sinb dc / 8pi^2
:return: the squared norm of the spherical harmonic with respect to given measure
:param l:
:param normalization:
:return:
"""
if normalized_haar:
return 1. / (2 * l + 1)
else:
return (8 * np.pi ** 2) / (2 * l + 1)
def wigner_d_naive(l, m, n, beta):
"""
Numerically naive implementation of the Wigner-d function.
This is useful for checking the correctness of other implementations.
:param l: the degree of the Wigner-d function. l >= 0
:param m: the order of the Wigner-d function. -l <= m <= l
:param n: the order of the Wigner-d function. -l <= n <= l
:param beta: the argument. 0 <= beta <= pi
:return: d^l_mn(beta) in the TODO: what basis? complex, quantum(?), centered, cs(?)
"""
from scipy.special import eval_jacobi
try:
from scipy.misc import factorial
except:
from scipy.special import factorial
from sympy.functions.special.polynomials import jacobi, jacobi_normalized
from sympy.abc import j, a, b, x
from sympy import N
#jfun = jacobi_normalized(j, a, b, x)
jfun = jacobi(j, a, b, x)
# eval_jacobi = lambda q, r, p, o: float(jfun.eval(int(q), int(r), int(p), float(o)))
# eval_jacobi = lambda q, r, p, o: float(N(jfun, int(q), int(r), int(p), float(o)))
eval_jacobi = lambda q, r, p, o: float(jfun.subs({j:int(q), a:int(r), b:int(p), x:float(o)}))
mu = np.abs(m - n)
nu = np.abs(m + n)
s = l - (mu + nu) / 2
xi = 1 if n >= m else (-1) ** (n - m)
# print(s, mu, nu, np.cos(beta), type(s), type(mu), type(nu), type(np.cos(beta)))
jac = eval_jacobi(s, mu, nu, np.cos(beta))
z = np.sqrt((factorial(s) * factorial(s + mu + nu)) / (factorial(s + mu) * factorial(s + nu)))
# print(l, m, n, beta, np.isfinite(mu), np.isfinite(nu), np.isfinite(s), np.isfinite(xi), np.isfinite(jac), np.isfinite(z))
assert np.isfinite(mu) and np.isfinite(nu) and np.isfinite(s) and np.isfinite(xi) and np.isfinite(jac) and np.isfinite(z)
assert np.isfinite(xi * z * np.sin(beta / 2) ** mu * np.cos(beta / 2) ** nu * jac)
return xi * z * np.sin(beta / 2) ** mu * np.cos(beta / 2) ** nu * jac
def wigner_d_naive_v2(l, m, n, beta):
"""
Wigner d functions as defined in the SOFT 2.0 documentation.
When approx_lim is set to a high value, this function appears to give
identical results to Johann Goetz' wignerd() function.
However, integration fails: does not satisfy orthogonality relations everywhere...
"""
from scipy.special import jacobi
if n >= m:
xi = 1
else:
xi = (-1)**(n - m)
mu = np.abs(m - n)
nu = np.abs(n + m)
s = l - (mu + nu) * 0.5
sq = np.sqrt((np.math.factorial(s) * np.math.factorial(s + mu + nu))
/ (np.math.factorial(s + mu) * np.math.factorial(s + nu)))
sinb = np.sin(beta * 0.5) ** mu
cosb = np.cos(beta * 0.5) ** nu
P = jacobi(s, mu, nu)(np.cos(beta))
return xi * sq * sinb * cosb * P
def wigner_d_naive_v3(l, m, n, approx_lim=1000000):
"""
Wigner "small d" matrix. (Euler z-y-z convention)
example:
l = 2
m = 1
n = 0
beta = linspace(0,pi,100)
wd210 = wignerd(l,m,n)(beta)
some conditions have to be met:
l >= 0
-l <= m <= l
-l <= n <= l
The approx_lim determines at what point
bessel functions are used. Default is when:
l > m+10
and
l > n+10
for integer l and n=0, we can use the spherical harmonics. If in
addition m=0, we can use the ordinary legendre polynomials.
"""
from scipy.special import jv, legendre, sph_harm, jacobi
try:
from scipy.misc import factorial, comb
except:
from scipy.special import factorial, comb
from numpy import floor, sqrt, sin, cos, exp, power
from math import pi
from scipy.special import jacobi
if (l < 0) or (abs(m) > l) or (abs(n) > l):
raise ValueError("wignerd(l = {0}, m = {1}, n = {2}) value error.".format(l, m, n) \
+ " Valid range for parameters: l>=0, -l<=m,n<=l.")
if (l > (m + approx_lim)) and (l > (n + approx_lim)):
#print 'bessel (approximation)'
return lambda beta: jv(m - n, l * beta)
if (floor(l) == l) and (n == 0):
if m == 0:
#print 'legendre (exact)'
return lambda beta: legendre(l)(cos(beta))
elif False:
#print 'spherical harmonics (exact)'
a = sqrt(4. * pi / (2. * l + 1.))
return lambda beta: a * sph_harm(m, l, beta, 0.).conj()
jmn_terms = {
l + n : (m - n, m - n),
l - n : (n - m, 0.),
l + m : (n - m, 0.),
l - m : (m - n, m - n),
}
k = min(jmn_terms)
a, lmb = jmn_terms[k]
b = 2. * l - 2. * k - a
if (a < 0) or (b < 0):
raise ValueError("wignerd(l = {0}, m = {1}, n = {2}) value error.".format(l, m, n) \
+ " Encountered negative values in (a,b) = ({0},{1})".format(a,b))
coeff = power(-1.,lmb) * sqrt(comb(2. * l - k, k + a)) * (1. / sqrt(comb(k + b, b)))
#print 'jacobi (exact)'
return lambda beta: coeff \
* power(sin(0.5*beta),a) \
* power(cos(0.5*beta),b) \
* jacobi(k,a,b)(cos(beta))
This diff is collapsed.
from functools import lru_cache
import numpy as np
import lie_learn.spaces.S2 as S2
def change_coordinates(coords, p_from='C', p_to='S'):
"""
Change Spherical to Cartesian coordinates and vice versa, for points x in S^3.
We use the following coordinate system:
https://en.wikipedia.org/wiki/N-sphere#Spherical_coordinates
Except that we use the order (alpha, beta, gamma), where beta ranges from 0 to pi while alpha and gamma range from
0 to 2 pi.
x0 = r * cos(alpha)
x1 = r * sin(alpha) * cos(gamma)
x2 = r * sin(alpha) * sin(gamma) * cos(beta)
x3 = r * sin(alpha * sin(gamma) * sin(beta)
:param conversion:
:param coords:
:return:
"""
if p_from == p_to:
return coords
elif p_from == 'S' and p_to == 'C':
alpha = coords[..., 0]
beta = coords[..., 1]
gamma = coords[..., 2]
r = 1.
out = np.empty(alpha.shape + (4,))
ca = np.cos(alpha)
cb = np.cos(beta)
cc = np.cos(gamma)
sa = np.sin(alpha)
sb = np.sin(beta)
sc = np.sin(gamma)
out[..., 0] = r * ca
out[..., 1] = r * sa * cc
out[..., 2] = r * sa * sc * cb
out[..., 3] = r * sa * sc * sb
return out
elif p_from == 'C' and p_to == 'S':
raise NotImplementedError
x = coords[..., 0]
y = coords[..., 1]
z = coords[..., 2]
w = coords[..., 3]
r = np.sqrt((coords ** 2).sum(axis=-1))
out = np.empty(x.shape + (3,))
out[..., 0] = np.arccos(z) # alpha
out[..., 1] = np.arctan2(y, x) # beta
out[..., 2] = np.arctan2(y, x) # gamma
return out
else:
raise ValueError('Unknown conversion:' + str(p_from) + ' to ' + str(p_to))
def linspace(b, grid_type='SOFT'):
"""
Compute a linspace on the 3-sphere.
Since S3 is ismorphic to SO(3), we use the grid grid_type from:
FFTs on the Rotation Group
Peter J. Kostelec and Daniel N. Rockmore
http://www.cs.dartmouth.edu/~geelong/soft/03-11-060.pdf
:param b:
:return:
"""
# alpha = 2 * np.pi * np.arange(2 * b) / (2. * b)
# beta = np.pi * (2 * np.arange(2 * b) + 1) / (4. * b)
# gamma = 2 * np.pi * np.arange(2 * b) / (2. * b)
beta, alpha = S2.linspace(b, grid_type)
# According to this paper:
# "Sampling sets and quadrature formulae on the rotation group"
# We can just tack a sampling grid for S^1 to a sampling grid for S^2 to get a sampling grid for SO(3).
gamma = 2 * np.pi * np.arange(2 * b) / (2. * b)
return alpha, beta, gamma
def meshgrid(b, grid_type='SOFT'):
return np.meshgrid(*linspace(b, grid_type), indexing='ij')
def integrate(f, normalize=True):
"""
Integrate a function f : S^3 -> R over the 3-sphere S^3, using the invariant integration measure
mu((alpha, beta, gamma)) = dalpha sin(beta) dbeta dgamma
i.e. this returns
int_S^3 f(x) dmu(x) = int_0^2pi int_0^pi int_0^2pi f(alpha, beta, gamma) dalpha sin(beta) dbeta dgamma
:param f: a function of three scalar variables returning a scalar.
:param normalize: if we use the measure dalpha sin(beta) dbeta dgamma,
the integral of f(a,b,c)=1 over the 3-sphere gives 8 pi^2.
If normalize=True, we divide the result of integration by this normalization constant, so that f integrates to 1.
In other words, use the normalized Haar measure.
:return: the integral of f over the 3-sphere
"""
from scipy.integrate import quad
f2 = lambda alpha, gamma: quad(lambda beta: f(alpha, beta, gamma) * np.sin(beta),
a=0,
b=np.pi)[0]
f3 = lambda alpha: quad(lambda gamma: f2(alpha, gamma),
a=0,
b=2 * np.pi)[0]
integral = quad(f3, 0, 2 * np.pi)[0]
if normalize:
return integral / (8 * np.pi ** 2)
else:
return integral
def integrate_quad(f, grid_type, normalize=True, w=None):
"""
Integrate a function f : SO(3) -> R, sampled on a grid of type grid_type, using quadrature weights w.
:param f: an ndarray containing function values on a grid
:param grid_type: the type of grid used to sample f
:param normalize: whether to use the normalized Haar measure or not
:param w: the quadrature weights. If not given, they are computed.
:return: the integral of f over S^2.
"""
if grid_type == 'SOFT':
b = f.shape[0] // 2
if w is None:
w = quadrature_weights(b, grid_type)
integral = np.sum(f * w[None, :, None])
else:
raise NotImplementedError('Unsupported grid_type:', grid_type)
if normalize:
return integral
else:
return integral * 8 * np.pi ** 2
@lru_cache(maxsize=32)
def quadrature_weights(b, grid_type='SOFT'):
"""
Compute quadrature weights for the grid used by Kostelec & Rockmore [1, 2].
This grid is:
alpha = 2 pi i / 2b
beta = pi (2 j + 1) / 4b
gamma = 2 pi k / 2b
where 0 <= i, j, k < 2b are indices
This grid can be obtained from the function: S3.linspace or S3.meshgrid
The quadrature weights for this grid are
w_B(j) = 2/b * sin(pi(2j + 1) / 4b) * sum_{k=0}^{b-1} 1 / (2 k + 1) sin((2j + 1)(2k + 1) pi / 4b)
This is eq. 23 in [1] and eq. 2.15 in [2].
[1] SOFT: SO(3) Fourier Transforms
Peter J. Kostelec and Daniel N. Rockmore
[2] FFTs on the Rotation Group
Peter J. Kostelec · Daniel N. Rockmore
:param b: bandwidth (grid has shape 2b * 2b * 2b)
:return: w: an array of length 2b containing the quadrature weigths
"""
if grid_type == 'SOFT':
k = np.arange(0, b)
w = np.array([(2. / b) * np.sin(np.pi * (2. * j + 1.) / (4. * b)) *
(np.sum((1. / (2 * k + 1))
* np.sin((2 * j + 1) * (2 * k + 1)
* np.pi / (4. * b))))
for j in range(2 * b)])
# This is not in the SOFT documentation, but we found that it is necessary to divide by this factor to
# get correct results.
w /= 2. * ((2 * b) ** 2)
# In the SOFT source, they talk about the following weights being used for
# odd-order transforms. Do not understand this, and the weights used above
# (defined in the SOFT papers) seems to work.
# w = np.array([(2. / b) *
# (np.sum((1. / (2 * k + 1))
# * np.sin((2 * j + 1) * (2 * k + 1)
# * np.pi / (4. * b))))
# for j in range(2 * b)])
return w
else:
raise NotImplementedError
\ No newline at end of file
"""
The n-Torus
"""
import numpy as np
def linspace(b, n=1, convention='regular'):
if convention == 'regular':
res = []
for i in range(n):
res.append(np.arange(b) * 2 * np.pi / b)
else:
raise ValueError('Unknown convention:' + convention)
return res
\ No newline at end of file
"""
n-dimensional real space, R^n.
"""
import numpy as np
# The following functions are part of the public interface of this module;
# other spaces / groups define their own meshgrid and linspace functions that work in an analogous way;
# for R^n the standard numpy functions fulfill this role.
from numpy import meshgrid, linspace
def change_coordinates(coords, n, p_from='C', p_to='S'):
"""
Change Spherical to Cartesian coordinates and vice versa.
todo: make this work for R^n and not just R^2, R^3
:param conversion:
:param coords:
:return:
"""
coords = np.asarray(coords)
if p_from == p_to:
return coords
if n == 2:
if (p_from == 'P' or p_from == 'polar') and (p_to == 'C' or p_to == 'cartesian'):
r = coords[..., 0]
theta = coords[..., 1]
out = np.empty_like(coords)
out[..., 0] = r * np.cos(theta)
out[..., 1] = r * np.sin(theta)
return out
elif (p_from == 'C' or p_from == 'cartesian') and (p_to == 'P' or p_to == 'polar'):
x = coords[..., 0]
y = coords[..., 1]
out = np.empty_like(coords)
out[..., 0] = np.sqrt(x ** 2 + y ** 2)
out[..., 1] = np.arctan2(y, x)
return out
elif (p_from == 'C' or p_from == 'cartesian') and (p_to == 'H' or p_to == 'homogeneous'):
x = coords[..., 0]
y = coords[..., 1]
out = np.empty(coords.shape[:-1] + (3,))
out[..., 0] = x
out[..., 1] = y
out[..., 2] = 1.
return out
elif (p_from == 'H' or p_from == 'homogeneous') and (p_to == 'C' or p_to == 'cartesian'):
xc = coords[..., 0]
yc = coords[..., 1]
c = coords[..., 2]
out = np.empty(coords.shape[:-1] + (2,))
out[..., 0] = xc / c
out[..., 1] = yc / c
return out
else:
raise ValueError('Unknown conversion' + str(p_from) + ' to ' + str(p_to))
elif n == 3:
if p_from == 'S' and p_to == 'C':
theta = coords[..., 0]
phi = coords[..., 1]
r = coords[..., 2]
out = np.empty(theta.shape + (3,))
ct = np.cos(theta)
cp = np.cos(phi)
st = np.sin(theta)
sp = np.sin(phi)
out[..., 0] = r * st * cp # x
out[..., 1] = r * st * sp # y
out[..., 2] = r * ct # z
return out
elif p_from == 'C' and p_to == 'S':
x = coords[..., 0]
y = coords[..., 1]
z = coords[..., 2]
out = np.empty_like(coords)
out[..., 2] = np.sqrt(x ** 2 + y ** 2 + z ** 2) # r
out[..., 0] = np.arccos(z / out[..., 2]) # theta
out[..., 1] = np.arctan2(y, x) # phi
return out
else:
raise ValueError('Unknown conversion:' + str(p_from) + ' to ' + str(p_to))
else:
raise ValueError('Only dimension n=2 and n=3 supported for now.')
def linspace(b, convention):
pass
This diff is collapsed.
class FFTBase(object):
def __init__(self):
pass
def analyze(self, f):
raise NotImplementedError('FFTBase.analyze should be implemented in subclass')
def synthesize(self, f_hat):
raise NotImplementedError('FFTBase.synthesize should be implemented in subclass')
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment