"""
Collection of sampling schemes for the sphere
"""
import urllib3
import numpy as np
from spharpy.samplings.coordinates import Coordinates, SamplingSphere
import spharpy
from ._eqsp import point_set as eq_point_set
[docs]def cube_equidistant(n_points):
"""Create a cuboid sampling with equidistant spacings in x, y, and z.
The cube will have dimensions 1 x 1 x 1
Parameters
----------
n_points : int, tuple
Number of points in the sampling. If a single value is given, the
number of sampling positions will be the same in every axis. If a tuple
is given, the number of points will be set as (n_x, n_y, n_z)
Returns
-------
sampling : Coordinates
Sampling positions as Coordinate object
"""
if np.size(n_points) == 1:
n_x = n_points
n_y = n_points
n_z = n_points
elif np.size(n_points) == 3:
n_x = n_points[0]
n_y = n_points[1]
n_z = n_points[2]
else:
raise ValueError("The number of points needs to be either an integer \
or a tuple with 3 elements.")
x = np.linspace(-1, 1, n_x)
y = np.linspace(-1, 1, n_y)
z = np.linspace(-1, 1, n_z)
x_grid, y_grid, z_grid = np.meshgrid(x, y, z)
return Coordinates(x_grid.flatten(), y_grid.flatten(), z_grid.flatten())
[docs]def hyperinterpolation(n_max):
"""Gives the points of a Hyperinterpolation sampling grid
after Sloan and Womersley [1]_.
Notes
-----
This implementation uses precalculated sets of points which are downloaded
from Womersley's homepage [2]_.
References
----------
.. [1] I. H. Sloan and R. S. Womersley, “Extremal Systems of Points and
Numerical Integration on the Sphere,” Advances in Computational
Mathematics, vol. 21, no. 1/2, pp. 107–125, 2004.
.. [2] http://web.maths.unsw.edu.au/~rsw/Sphere/Extremal/New/index.html
Parameters
----------
n_max : integer
Spherical harmonic order of the sampling
Returns
-------
sampling: SamplingSphere
SamplingSphere object containing all sampling points
"""
n_sh = (n_max+1)**2
filename = "md%03d.%05d" % (n_max, n_sh)
url = "https://web.maths.unsw.edu.au/~rsw/Sphere/S2Pts/MD/"
fileurl = url + filename
http = urllib3.PoolManager(cert_reqs=False)
http_data = http.urlopen('GET', fileurl)
if http_data.status == 200:
file_data = http_data.data.decode()
else:
raise ConnectionError("Connection error. Please check your internet \
connection.")
file_data = np.fromstring(
file_data, dtype='double', sep=' ').reshape((n_sh, 4))
sampling = SamplingSphere(
file_data[:, 0],
file_data[:, 1],
file_data[:, 2])
sampling.weights = file_data[:, 3]
return sampling
[docs]def spherical_t_design(n_max, criterion='const_energy'):
r"""Return the sampling positions for a spherical t-design [3]_ .
For a spherical harmonic order N, a t-Design of degree `:math: t=2N` for
constant energy or `:math: t=2N+1` additionally ensuring a constant angular
spread of energy is required [4]_. For a given degree t
.. math::
L = \lceil \frac{(t+1)^2}{2} \rceil+1,
points will be generated, except for t = 3, 5, 7, 9, 11, 13, and 15.
T-designs allow for a inverse spherical harmonic transform matrix
calculated as `:math: D = \frac{4\pi}{L} \mathbf{Y}^\mathrm{H}`.
Parameters
----------
degree : integer
T-design degree
criterion : 'const_energy', 'const_angular_spread'
Design criterion ensuring only a constant energy or additionally
constant angular spread of energy
Returns
-------
sampling : SamplingSphere
SamplingSphere object containing all sampling points
Notes
-----
This function downloads a pre-calculated set of points from
Rob Womersley's homepage [5]_ .
References
----------
.. [3] C. An, X. Chen, I. H. Sloan, and R. S. Womersley, “Well Conditioned
Spherical Designs for Integration and Interpolation on the
Two-Sphere,” SIAM Journal on Numerical Analysis, vol. 48, no. 6,
pp. 2135–2157, Jan. 2010.
.. [4] F. Zotter, M. Frank, and A. Sontacchi, “The Virtual T-Design
Ambisonics-Rig Using VBAP,” in Proceedings on the Congress on
Sound and Vibration, 2010.
.. [5] http://web.maths.unsw.edu.au/~rsw/Sphere/EffSphDes/sf.html
"""
if criterion == 'const_energy':
degree = 2*n_max
elif criterion == 'const_angular_spread':
degree = 2*n_max + 1
else:
raise ValueError("Invalid design criterion.")
n_points = int(np.ceil((degree + 1)**2 / 2) + 1)
n_points_exceptions = {3: 8, 5: 18, 7: 32, 9: 50, 11: 72, 13: 98, 15: 128}
if degree in n_points_exceptions:
n_points = n_points_exceptions[degree]
filename = "sf%03d.%05d" % (degree, n_points)
url = "http://web.maths.unsw.edu.au/~rsw/Sphere/Points/SF/SF29-Nov-2012/"
fileurl = url + filename
http = urllib3.PoolManager(
cert_reqs=False)
http_data = http.urlopen('GET', fileurl)
if http_data.status == 200:
file_data = http_data.data.decode()
elif http_data.status == 404:
raise FileNotFoundError("File was not found. Check if the design you \
are trying to calculate is a valid t-design.")
else:
raise ConnectionError("Connection error. Please check your internet \
connection.")
points = np.fromstring(
file_data,
dtype=float,
sep=' ').reshape((n_points, 3)).T
return SamplingSphere.from_array(points)
[docs]def dodecahedron():
"""Generate a sampling based on the center points of the twelve
dodecahedron faces.
Returns
-------
rad : ndarray
Radius of the sampling points
theta : ndarray
Elevation angle in the range [0, pi]
phi : ndarray
Azimuth angle in the range [0, 2 pi]
Returns
-------
sampling : SamplingSphere
SamplingSphere object containing all sampling points
"""
dihedral = 2*np.arcsin(np.cos(np.pi/3)/np.sin(np.pi/5))
R = np.tan(np.pi/3)*np.tan(dihedral/2)
rho = np.cos(np.pi/5)/np.sin(np.pi/10)
theta1 = np.arccos((np.cos(np.pi/5)/np.sin(np.pi/5))/np.tan(np.pi/3))
a2 = 2*np.arccos(rho/R)
theta2 = theta1+a2
theta3 = np.pi - theta2
theta4 = np.pi - theta1
phi1 = 0
phi2 = 2*np.pi/3
phi3 = 4*np.pi/3
theta = np.concatenate((
np.tile(theta1, 3), np.tile(theta2, 3),
np.tile(theta3, 3), np.tile(theta4, 3)))
phi = np.tile(np.array([
phi1, phi2, phi3,
phi1 + np.pi/3, phi2 + np.pi/3, phi3 + np.pi/3]), 2)
rad = np.ones(np.size(theta))
return SamplingSphere.from_spherical(rad, theta, phi)
[docs]def icosahedron():
"""Generate a sampling based on the center points of the twenty \
icosahedron faces.
Returns
-------
sampling : SamplingSphere
SamplingSphere object containing all sampling points
"""
gamma_R_r = np.arccos(np.cos(np.pi/3) / np.sin(np.pi/5))
gamma_R_rho = np.arccos(1/(np.tan(np.pi/5) * np.tan(np.pi/3)))
theta = np.tile(np.array([np.pi - gamma_R_rho,
np.pi - gamma_R_rho - 2*gamma_R_r,
2*gamma_R_r + gamma_R_rho,
gamma_R_rho]), 5)
theta = np.sort(theta)
phi = np.arange(0, 2*np.pi, 2*np.pi/5)
phi = np.concatenate((np.tile(phi, 2), np.tile(phi + np.pi/5, 2)))
rad = np.ones(20)
return SamplingSphere.from_spherical(rad, theta, phi)
[docs]def equiangular(n_max):
"""Generate an equiangular sampling of the sphere.
Paramters
---------
n_max : integer
Spherical harmonic order of the sampling
Returns
-------
sampling : SamplingSphere
SamplingSphere object containing all sampling points
"""
n_theta = np.round((n_max+1)*2)
n_phi = n_theta
theta_angles = np.arange(np.pi/(n_theta*2),
np.pi,
np.pi/n_theta)
phi_angles = np.arange(0,
2*np.pi,
2*np.pi/n_phi)
theta, phi = np.meshgrid(theta_angles, phi_angles)
rad = np.ones(theta.size)
# calculate weights
L = 2*np.arange(0, n_max + 1) + 1
factor_phi = 2*np.pi/n_phi
factor_theta = 2/n_theta
factor_sin = 4/np.pi * np.sin(theta_angles) * \
(1/L @ L[np.newaxis].T @ theta_angles[np.newaxis])
weights = np.tile(factor_phi * factor_theta * np.pi/2 * factor_sin, n_phi)
sampling = SamplingSphere.from_spherical(
rad, theta.reshape(-1), phi.reshape(-1))
sampling.weights = weights
return sampling
[docs]def gaussian(n_max):
"""Generate sampling of the sphere based on the Gaussian quadrature.
Paramters
---------
n_max : integer
Spherical harmonic order of the sampling
Returns
-------
sampling : SamplingSphere
SamplingSphere object containing all sampling points
"""
legendre, weights = np.polynomial.legendre.leggauss(n_max+1)
theta_angles = np.arccos(legendre)
n_phi = np.round((n_max+1)*2)
phi_angles = np.arange(0,
2*np.pi,
2*np.pi/n_phi)
theta, phi = np.meshgrid(theta_angles, phi_angles)
rad = np.ones(theta.size)
weights = np.tile(weights*np.pi/(n_max+1), 2*(n_max+1))
sampling = SamplingSphere.from_spherical(
rad, theta.reshape(-1), phi.reshape(-1))
sampling.weights = weights
return sampling
[docs]def eigenmike_em32():
"""Microphone positions of the Eigenmike em32 by mhacoustics according to
the Eigenstudio user manual on the homepage [6]_.
References
----------
.. [6] Eigenstudio User Manual, https://mhacoustics.com/download
Returns
-------
sampling : SamplingSphere
SamplingSphere object containing all sampling points
"""
rad = np.ones(32)
theta = np.array([69.0, 90.0, 111.0, 90.0, 32.0, 55.0,
90.0, 125.0, 148.0, 125.0, 90.0, 55.0,
21.0, 58.0, 121.0, 159.0, 69.0, 90.0,
111.0, 90.0, 32.0, 55.0, 90.0, 125.0,
148.0, 125.0, 90.0, 55.0, 21.0, 58.0,
122.0, 159.0]) * np.pi / 180
phi = np.array([0.0, 32.0, 0.0, 328.0, 0.0, 45.0,
69.0, 45.0, 0, 315.0, 291.0, 315.0,
91.0, 90.0, 90.0, 89.0, 180.0, 212.0,
180.0, 148.0, 180.0, 225.0, 249.0, 225.0,
180.0, 135.0, 111.0, 135.0, 269.0, 270.0,
270.0, 271.0]) * np.pi / 180
return SamplingSphere.from_spherical(rad, theta, phi)
[docs]def icosahedron_ke4():
"""Microphone positions of the KE4 spherical microphone array.
The microphone marked as "1" defines the positive x-axis.
Returns
-------
sampling : SamplingSphere
SamplingSphere object containing all sampling points
"""
theta = np.array([1.565269801525254, 2.294997457752220, 1.226592351686568,
1.226592351686568, 1.696605844149543, 2.409088979442091,
2.409088979442090, 1.696605844149543, 0.506378251408914,
0.506378251408914, 2.635214402180879, 1.444986809440250,
0.732503674147703, 0.732503674147703, 1.444986809440250,
2.635214402180879, 1.915000301903226, 0.846595195837573,
1.915000301903225, 1.576322852064539])
phi = np.array([0.000000000000000, 6.283185307179586, 0.660263824203969,
5.622921482975618, 5.055791576545843, 5.241315660913181,
1.041869646266405, 1.227393730633743, 0.826693168093822,
5.456492139085764, 3.968285821683615, 4.368986384223536,
4.183462299856198, 2.099723007323388, 1.914198922956050,
2.314899485495971, 2.481328829385824, 3.141592653589793,
3.801856477793762, 3.141592653589793])
rad = np.ones(20) * 0.065
return SamplingSphere.from_spherical(rad, theta, phi)
[docs]def equalarea(n_max, condition_num=2.5, n_points=None):
"""Sampling based on partitioning into faces with equal area [9]_.
Parameters
----------
n_max : int
Spherical harmonic order
condition_num : double
Desired maximum condition number of the spherical harmonic basis matrix
n_points : int, optional
Number of points to start the condition number optimization. If set to
None n_points will be (n_max+1)**2
Returns
-------
sampling : SamplingSphere
SamplingSphere object containing all sampling points
References
----------
.. [9] P. Leopardi, “A partition of the unit sphere into regions of equal
area and small diameter,” Electronic Transactions on Numerical
Analysis, vol. 25, no. 12, pp. 309–327, 2006.
"""
if not n_points:
n_points = (n_max+1)**2
while True:
point_set = eq_point_set(2, n_points)
sampling = SamplingSphere(point_set[0], point_set[1], point_set[2])
if condition_num == np.inf:
break
Y = spharpy.spherical.spherical_harmonic_basis(n_max, sampling)
cond = np.linalg.cond(Y)
if cond < condition_num:
break
n_points += 1
sampling.n_max = n_max
return sampling
[docs]def spiral_points(n_max, condition_num=2.5, n_points=None):
"""Sampling based on a spiral distribution of points on a sphere [10]_.
Parameters
----------
n_max : int
Spherical harmonic order
condition_num : double
Desired maximum condition number of the spherical harmonic basis matrix
n_points : int, optional
Number of points to start the condition number optimization. If set to
None n_points will be (n_max+1)**2
Returns
-------
sampling : SamplingSphere
SamplingSphere object containing all sampling points
References
----------
.. [10] E. a. Rakhmanov, E. B. Saff, and Y. M. Zhou, “Minimal Discrete
Energy on the Sphere,” Mathematical Research Letters, vol. 1,
no. 6, pp. 647–662, 1994.
"""
if n_points is None:
n_points = (n_max+1)**2
def _spiral_points(n_points):
"""Helper function doing the actual calculation of the points"""
r = np.zeros(n_points)
h = np.zeros(n_points)
theta = np.zeros(n_points)
phi = np.zeros(n_points)
p = 1/2
a = 1 - 2*p/(n_points-3)
b = p*(n_points+1)/(n_points-3)
r[0] = 0
theta[0] = np.pi
phi[0] = 0
# Then for k stepping by 1 from 2 to n-1:
for k in range(1, n_points-1):
kStrich = a*k + b
h[k] = -1 + 2*(kStrich-1)/(n_points-1)
r[k] = np.sqrt(1-h[k]**2)
theta[k] = np.arccos(h[k])
phi[k] = np.mod(
(phi[k-1]) + 3.6/np.sqrt(n_points)*2/(r[k-1]+r[k]), 2*np.pi)
# Finally:
theta[n_points-1] = 0
phi[n_points-1] = 0
return theta, phi
while True:
theta, phi = _spiral_points(n_points)
sampling = SamplingSphere.from_spherical(np.ones(n_points), theta, phi)
if condition_num == np.inf:
break
Y = spharpy.spherical.spherical_harmonic_basis(n_max, sampling)
cond = np.linalg.cond(Y)
if cond < condition_num:
break
n_points += 1
sampling.n_max = n_max
return sampling