"""Module for spherical sampling grids."""
import numpy as np
import urllib3
from urllib3.exceptions import InsecureRequestWarning
import warnings
import os
import scipy.io as sio
import pyfar
from pyfar.classes.warnings import PyfarDeprecationWarning
from . import external
[docs]
def cart_equidistant_cube(n_points):
"""
Create a cuboid sampling with equidistant spacings in x, y, and z.
This function will be deprecated in pyfar 0.8.0 in favor
of :py:func:`spharpy.samplings.cube_equidistant`.
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. Does not contain sampling weights.
"""
warnings.warn((
"This function will be deprecated in pyfar 0.8.0 in favor "
"of spharpy.samplings.cube_equidistant."),
PyfarDeprecationWarning, stacklevel=2)
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)
sampling = pyfar.Coordinates(
x_grid.flatten(),
y_grid.flatten(),
z_grid.flatten(),
comment='equidistant cuboid sampling grid')
return sampling
[docs]
def sph_dodecahedron(radius=1.):
"""
Generate a sampling based on the center points of the twelve
dodecahedron faces.
This function will be deprecated in pyfar 0.8.0 in favor
of :py:func:`spharpy.samplings.dodecahedron`.
Parameters
----------
radius : number, optional
Radius of the sampling grid. The default is ``1``.
Returns
-------
sampling : Coordinates
Sampling positions. Sampling weights can be obtained from
:py:func:`calculate_sph_voronoi_weights`.
"""
warnings.warn((
"This function will be deprecated in pyfar 0.8.0 in favor "
"of spharpy.samplings.dodecahedron."),
PyfarDeprecationWarning, stacklevel=2)
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 = radius * np.ones(np.size(theta))
sampling = pyfar.Coordinates.from_spherical_colatitude(
phi, theta, rad,
comment='dodecahedral sampling grid')
return sampling
[docs]
def sph_icosahedron(radius=1.):
"""
Generate a sampling from the center points of the twenty icosahedron faces.
This function will be deprecated in pyfar 0.8.0 in favor
of :py:func:`spharpy.samplings.icosahedron`.
Parameters
----------
radius : number, optional
Radius of the sampling grid. The default is ``1``.
Returns
-------
sampling : Coordinates
Sampling positions. Sampling weights can be obtained from
:py:func:`calculate_sph_voronoi_weights`.
"""
warnings.warn((
"This function will be deprecated in pyfar 0.8.0 in favor "
"of spharpy.samplings.icosahedron."),
PyfarDeprecationWarning, stacklevel=2)
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 = radius * np.ones(20)
sampling = pyfar.Coordinates.from_spherical_colatitude(
phi, theta, rad,
comment='icosahedral spherical sampling grid')
return sampling
[docs]
def sph_equiangular(n_points=None, sh_order=None, radius=1.):
"""
Generate an equiangular sampling of the sphere.
This function will be deprecated in pyfar 0.8.0 in favor
of :py:func:`spharpy.samplings.equiangular`.
For detailed information, see [#]_, Chapter 3.2.
This sampling does not contain points at the North and South Pole and is
typically used for spherical harmonics processing. See
:py:func:`sph_equal_angle` and :py:func:`sph_great_circle` for samplings
containing points at the poles.
Parameters
----------
n_points : int, tuple of two ints
Number of sampling points in azimuth and elevation. Either `n_points`
or `sh_order` must be provided. The default is ``None``.
sh_order : int
Maximum applicable spherical harmonic order. If this is provided,
'n_points' is set to ``2 * sh_order + 1``. Either `n_points` or
`sh_order` must be provided. The default is ``None``.
radius : number, optional
Radius of the sampling grid. The default is ``1``.
Returns
-------
sampling : Coordinates
Sampling positions including sampling weights.
References
----------
.. [#] B. Rafaely, Fundamentals of spherical array processing, 1st ed.
Berlin, Heidelberg, Germany: Springer, 2015.
"""
warnings.warn((
"This function will be deprecated in pyfar 0.8.0 in favor "
"of spharpy.samplings.equiangular."),
PyfarDeprecationWarning, stacklevel=2)
if (n_points is None) and (sh_order is None):
raise ValueError(
"Either the n_points or sh_order needs to be specified.")
# get number of points from required spherical harmonic order
# ([#], equation 3.4)
if sh_order is not None:
n_points = 2 * (int(sh_order) + 1)
# get the angles
n_points = np.asarray(n_points)
if n_points.size == 2:
n_phi = n_points[0]
n_theta = n_points[1]
else:
n_phi = n_points
n_theta = n_points
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)
# construct the sampling grid
theta, phi = np.meshgrid(theta_angles, phi_angles)
rad = radius * np.ones(theta.size)
# compute maximum applicable spherical harmonic order
if sh_order is None:
n_max = int(np.min([n_phi / 2 - 1, n_theta / 2 - 1]))
else:
n_max = int(sh_order)
# compute sampling weights ([1], equation 3.11)
q = 2 * np.arange(0, n_max + 1) + 1
w = np.zeros_like(theta_angles)
for nn, tt in enumerate(theta_angles):
w[nn] = 2 * np.pi / (n_max + 1)**2 * np.sin(tt) \
* np.sum(1 / q * np.sin(q * tt))
# repeat and normalize sampling weights
w = np.tile(w, n_phi)
w = w / np.sum(w)
# make Coordinates object
sampling = pyfar.Coordinates(
phi.reshape(-1), theta.reshape(-1), rad, 'sph', 'top_colat',
comment='equiangular spherical sampling grid',
weights=w, sh_order=n_max)
return sampling
[docs]
def sph_gaussian(n_points=None, sh_order=None, radius=1.):
"""
Generate sampling of the sphere based on the Gaussian quadrature.
This function will be deprecated in pyfar 0.8.0 in favor
of :py:func:`spharpy.samplings.gaussian`.
For detailed information, see [#]_ (Section 3.3).
This sampling does not contain points at the North and South Pole and is
typically used for spherical harmonics processing. See
:py:func:`sph_equal_angle` and :py:func:`sph_great_circle` for samplings
containing points at the poles.
Parameters
----------
n_points : int, tuple of two ints
Number of sampling points in azimuth and elevation. Either `n_points`
or `sh_order` must be provided. The default is ``None``.
sh_order : int
Maximum applicable spherical harmonic order. If this is provided,
`n_points` is set to ``(2 * (sh_order + 1), sh_order + 1)``. Either
`n_points` or `sh_order` must be provided. The default is ``None``.
radius : number, optional
Radius of the sampling grid in meters. The default is ``1``.
Returns
-------
sampling : Coordinates
Sampling positions including sampling weights.
References
----------
.. [#] B. Rafaely, Fundamentals of spherical array processing, 1st ed.
Berlin, Heidelberg, Germany: Springer, 2015.
"""
warnings.warn((
"This function will be deprecated in pyfar 0.8.0 in favor "
"of spharpy.samplings.gaussian."),
PyfarDeprecationWarning, stacklevel=2)
if (n_points is None) and (sh_order is None):
raise ValueError(
"Either the n_points or sh_order needs to be specified.")
# get number of points from required spherical harmonic order
# ([1], equation 3.4)
if sh_order is not None:
n_points = [2 * (int(sh_order) + 1), int(sh_order) + 1]
# get the number of points in both dimensions
n_points = np.asarray(n_points)
if n_points.size == 2:
n_phi = n_points[0]
n_theta = n_points[1]
else:
n_phi = n_points
n_theta = n_points
# compute the maximum applicable spherical harmonic order
if sh_order is None:
n_max = int(np.min([n_phi / 2 - 1, n_theta - 1]))
else:
n_max = int(sh_order)
# construct the sampling grid
legendre, weights = np.polynomial.legendre.leggauss(int(n_theta))
theta_angles = np.arccos(legendre)
phi_angles = np.arange(0, 2 * np.pi, 2 * np.pi / n_phi)
theta, phi = np.meshgrid(theta_angles, phi_angles)
rad = radius * np.ones(theta.size)
# compute the sampling weights
weights = np.tile(weights, n_phi)
weights = weights / np.sum(weights)
# make Coordinates object
sampling = pyfar.Coordinates(
phi.reshape(-1), theta.reshape(-1), rad, 'sph', 'top_colat',
comment='gaussian spherical sampling grid',
weights=weights, sh_order=n_max)
return sampling
[docs]
def sph_extremal(n_points=None, sh_order=None, radius=1.):
"""
Return a Hyperinterpolation sampling grid.
This function will be deprecated in pyfar 0.8.0 in favor
of :py:func:`spharpy.samplings.hyperinterpolation`.
After Sloan and Womersley [#]_. The samplings are available for
1 <= `sh_order` <= 200 (``n_points = (sh_order + 1)^2``).
Parameters
----------
n_points : int
Number of sampling points in the grid. Related to the spherical
harmonic order by ``n_points = (sh_order + 1)**2``. Either `n_points`
or `sh_order` must be provided. The default is ``None``.
sh_order : int
Maximum applicable spherical harmonic order. Related to the number of
points by ``sh_order = np.sqrt(n_points) - 1``. Either `n_points` or
`sh_order` must be provided. The default is ``None``.
radius : number, optional
Radius of the sampling grid in meters. The default is ``1``.
Returns
-------
sampling : Coordinates
Sampling positions including sampling weights.
Notes
-----
This implementation uses precalculated sets of points from [#]_. The data
up to ``sh_order = 99`` are loaded the first time this function is called.
The remaining data is loaded upon request.
References
----------
.. [#] 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.
.. [#] https://web.maths.unsw.edu.au/~rsw/Sphere/MaxDet/
"""
warnings.warn((
"This function will be deprecated in pyfar 0.8.0 in favor "
"of spharpy.samplings.hyperinterpolation."),
PyfarDeprecationWarning, stacklevel=2)
if (n_points is None) and (sh_order is None):
for o in range(1, 100):
print(f"SH order {o}, number of points {(o + 1)**2}")
return None
# check input
if n_points is not None and sh_order is not None:
raise ValueError("Either n_points or sh_order must be None.")
# get number of points or spherical harmonic order
if sh_order is not None:
if sh_order < 1 or sh_order > 200:
raise ValueError('sh_order must be between 1 and 200')
n_points = (sh_order + 1)**2
else:
if n_points not in [(n + 1)**2 for n in range(1, 200)]:
raise ValueError('invalid value for n_points')
sh_order = np.sqrt(n_points) - 1
# download data if necessary
filename = "samplings_extremal_md%03d.%05d" % (sh_order, n_points)
filename = os.path.join(os.path.dirname(__file__), "external", filename)
if not os.path.exists(filename):
if sh_order < 100:
_sph_extremal_load_data('all')
else:
_sph_extremal_load_data(sh_order)
# open data
with open(filename, 'rb') as f:
file_data = f.read()
# format data
file_data = file_data.decode()
file_data = np.fromstring(
file_data,
dtype='double',
sep=' ').reshape((int(n_points), 4))
# normalize weights
weights = file_data[:, 3] / 4 / np.pi
# generate Coordinates object
sampling = pyfar.Coordinates(
file_data[:, 0] * radius,
file_data[:, 1] * radius,
file_data[:, 2] * radius,
sh_order=sh_order, weights=weights,
comment='extremal spherical sampling grid')
return sampling
[docs]
def sph_t_design(degree=None, sh_order=None, criterion='const_energy',
radius=1.):
r"""
Return spherical t-design sampling grid.
This function will be deprecated in pyfar 0.8.0 in favor
of :py:func:`spharpy.samplings.spherical_t_design`.
For detailed information, see [#]_.
For a spherical harmonic order :math:`n_{sh}`, a t-Design of degree
:math:`t=2n_{sh}` for constant energy or :math:`t=2n_{sh}+1` additionally
ensuring a constant angular spread of energy is required [#]_. 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 an inverse spherical harmonic transform matrix
calculated as :math:`D = \frac{4\pi}{L} \mathbf{Y}^\mathrm{H}` with
:math:`\mathbf{Y}^\mathrm{H}` being the hermitian transpose of the
spherical harmonics matrix.
Parameters
----------
degree : int
T-design degree between ``1`` and ``180``. Either `degree` or
`sh_order` must be provided. The default is ``None``.
sh_order : int
Maximum applicable spherical harmonic order. Related to the degree
by ``degree = 2 * sh_order`` (``const_energy``) and
``degree = 2 * sh_order + 1`` (``const_angular_spread``). Either
`degree` or `sh_order` must be provided. The default is ``None``.
criterion : ``const_energy``, ``const_angular_spread``
Design criterion ensuring only a constant energy or additionally
constant angular spread of energy. The default is ``const_energy``.
radius : number, optional
Radius of the sampling grid in meters. The default is ``1``.
Returns
-------
sampling : Coordinates
Sampling positions. Sampling weights can be obtained from
:py:func:`calculate_sph_voronoi_weights`.
Notes
-----
This function downloads a pre-calculated set of points from [#]_ . The data
up to ``degree = 99`` are loaded the first time this function is called.
The remaining data is loaded upon request.
References
----------
.. [#] 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.
.. [#] 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.
.. [#] http://web.maths.unsw.edu.au/~rsw/Sphere/EffSphDes/sf.html
"""
warnings.warn((
"This function will be deprecated in pyfar 0.8.0 in favor "
"of spharpy.samplings.spherical_t_design."),
PyfarDeprecationWarning, stacklevel=2)
# check input
if (degree is None) and (sh_order is None):
print('Possible input values:')
for d in range(1, 181):
print(f"degree {d}, sh_order {int(d / 2)} ('const_energy'), \
{int((d - 1) / 2)} ('const_angular_spread')")
return None
if criterion not in ['const_energy', 'const_angular_spread']:
raise ValueError("Invalid design criterion. Must be 'const_energy' \
or 'const_angular_spread'.")
if degree is not None and sh_order is not None:
raise ValueError("Either n_points or sh_order must be None.")
# get the degree
if degree is None:
if criterion == 'const_energy':
degree = 2 * sh_order
else:
degree = 2 * sh_order + 1
# get the SH order for the meta data entry in the Coordinates object
else:
if criterion == 'const_energy':
sh_order = int(degree / 2)
else:
sh_order = int((degree - 1) / 2)
if degree < 1 or degree > 180:
raise ValueError('degree must be between 1 and 180.')
# get the number of points
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]
# download data if neccessary
filename = "samplings_t_design_sf%03d.%05d" % (degree, n_points)
filename = os.path.join(os.path.dirname(__file__), "external", filename)
if not os.path.exists(filename):
if degree < 100:
_sph_t_design_load_data('all')
else:
_sph_t_design_load_data(degree)
# open data
with open(filename, 'rb') as f:
file_data = f.read()
# format data
file_data = file_data.decode()
points = np.fromstring(
file_data,
dtype=np.double,
sep=' ').reshape((n_points, 3))
# generate Coordinates object
sampling = pyfar.Coordinates(
points[..., 0] * radius,
points[..., 1] * radius,
points[..., 2] * radius,
sh_order=sh_order,
comment='spherical T-design sampling grid')
return sampling
[docs]
def sph_equal_angle(delta_angles, radius=1.):
"""
Generate sampling of the sphere with equally spaced angles.
This function will be deprecated in pyfar 0.8.0 in favor
of :py:func:`spharpy.samplings.equal_angle`.
This sampling contain points at the North and South Pole. See
:py:func:`sph_equiangular`, :py:func:`sph_gaussian`, and
:py:func:`sph_great_circle` for samplings that do not contain points at the
poles.
Parameters
----------
delta_angles : tuple, number
Tuple that gives the angular spacing in azimuth and colatitude in
degrees. If a number is provided, the same spacing is applied in both
dimensions.
radius : number, optional
Radius of the sampling grid. The default is ``1``.
Returns
-------
sampling : Coordinates
Sampling positions. Sampling weights can be obtained from
:py:func:`calculate_sph_voronoi_weights`.
"""
warnings.warn((
"This function will be deprecated in pyfar 0.8.0 in favor "
"of spharpy.samplings.equal_angle."),
PyfarDeprecationWarning, stacklevel=2)
# get the angles
delta_angles = np.asarray(delta_angles)
if delta_angles.size == 2:
delta_phi = delta_angles[0]
delta_theta = delta_angles[1]
else:
delta_phi = delta_angles
delta_theta = delta_angles
# check if the angles can be distributed
eps = np.finfo('float').eps
if not (np.abs(360 % delta_phi) < 2*eps):
raise ValueError("delta_phi must be an integer divisor of 360")
if not (np.abs(180 % delta_theta) < 2*eps):
raise ValueError("delta_theta must be an integer divisor of 180")
# get the angles
phi_angles = np.arange(0, 360, delta_phi)
theta_angles = np.arange(delta_theta, 180, delta_theta)
# stack the angles
phi = np.tile(phi_angles, theta_angles.size)
theta = np.repeat(theta_angles, phi_angles.size)
# add North and South Pole
phi = np.concatenate(([0], phi, [0]))
theta = np.concatenate(([0], theta, [180]))
# make Coordinates object
sampling = pyfar.Coordinates.from_spherical_colatitude(
phi/180*np.pi, theta/180*np.pi, radius,
comment='equal angle spherical sampling grid')
return sampling
[docs]
def sph_great_circle(elevation=np.linspace(-90, 90, 19), gcd=10, radius=1,
azimuth_res=1, match=360):
r"""
Spherical sampling grid according to the great circle distance criterion.
This function will be deprecated in pyfar 0.8.0 in favor
of :py:func:`spharpy.samplings.great_circle`.
Sampling grid where neighboring points of the same elevation have approx.
the same great circle distance across elevations [#]_.
Parameters
----------
elevation : array like, optional
Contains the elevation from which the sampling grid is generated, with
:math:`-90^\circ\leq elevation \leq 90^\circ` (:math:`90^\circ`:
North Pole, :math:`-90^\circ`: South Pole). The default is
``np.linspace(-90, 90, 19)``.
gcd : number, optional
Desired great circle distance (GCD). Note that the actual GCD of the
sampling grid is equal or smaller then the desired GCD and that the GCD
may vary across elevations. The default is ``10``.
radius : number, optional
Radius of the sampling grid in meters. The default is ``1``.
azimuth_res : number, optional
Minimum resolution of the azimuth angle in degree. The default is
``1``.
match : number, optional
Forces azimuth entries to appear with a period of match degrees. E.g.,
if ``match=90``, the grid includes the azimuth angles 0, 90, 180, and
270 degrees. The default is ``360``.
Returns
-------
sampling : Coordinates
Sampling positions. Sampling weights can be obtained from
:py:func:`calculate_sph_voronoi_weights`.
References
----------
.. [#] B. P. Bovbjerg, F. Christensen, P. Minnaar, and X. Chen, “Measuring
the head-related transfer functions of an artificial head with a
high directional resolution,” 109th AES Convention, Los Angeles,
USA, Sep. 2000.
"""
warnings.warn((
"This function will be deprecated in pyfar 0.8.0 in favor "
"of spharpy.samplings.great_circle."),
PyfarDeprecationWarning, stacklevel=2)
# check input
assert 1 / azimuth_res % 1 == 0, "1/azimuth_res must be an integer."
assert not 360 % match, "360/match must be an integer."
assert match / azimuth_res % 1 == 0, "match/azimuth_res must be an \
integer."
elevation = np.atleast_1d(np.asarray(elevation))
# calculate delta azimuth to meet the desired great circle distance.
# (according to Bovbjerg et al. 2000: Measuring the head related transfer
# functions of an artificial head with a high directional azimuth_res)
d_az = 2 * np.arcsin(np.clip(
np.sin(gcd / 360 * np.pi) / np.cos(elevation / 180 * np.pi), -1, 1))
d_az = d_az / np.pi * 180
# correct values at the poles
d_az[np.abs(elevation) == 90] = 360
# next smallest value in desired angular azimuth_res
d_az = d_az // azimuth_res * azimuth_res
# adjust phi to make sure that: match // d_az == 0
for nn in range(d_az.size):
if abs(elevation[nn]) != 90:
while match % d_az[nn] > 1e-15:
# round to precision of azimuth_res to avoid numerical errors
d_az[nn] = np.round((d_az[nn] - azimuth_res)/azimuth_res) \
* azimuth_res
# construct the full sampling grid
azim = np.empty(0)
elev = np.empty(0)
for nn in range(elevation.size):
azim = np.append(azim, np.arange(0, 360, d_az[nn]))
elev = np.append(elev, np.full(int(360 / d_az[nn]), elevation[nn]))
# round to precision of azimuth_res to avoid numerical errors
azim = np.round(azim/azimuth_res) * azimuth_res
# make Coordinates object
sampling = pyfar.Coordinates.from_spherical_elevation(
azim/180*np.pi, elev/180*np.pi, radius,
comment='spherical great circle sampling grid')
return sampling
[docs]
def sph_lebedev(n_points=None, sh_order=None, radius=1.):
"""
Return Lebedev spherical sampling grid.
This function will be deprecated in pyfar 0.8.0 in favor
of :py:func:`spharpy.samplings.lebedev`.
For detailed information, see [#]_. For a list of available values
for `n_points` and `sh_order` call :py:func:`lebedev`.
Parameters
----------
n_points : int, optional
Number of sampling points in the grid. Related to the spherical
harmonic order by ``n_points = (sh_order + 1)**2``. Either `n_points`
or `sh_order` must be provided. The default is ``None``.
sh_order : int, optional
Maximum applicable spherical harmonic order. Related to the number of
points by ``sh_order = np.sqrt(n_points) - 1``. Either `n_points` or
`sh_order` must be provided. The default is ``None``.
radius : number, optional
Radius of the sampling grid in meters. The default is ``1``.
Returns
-------
sampling : Coordinates
Sampling positions including sampling weights.
Notes
-----
This is a Python port of the Matlab Code written by Rob Parrish [#]_.
References
----------
.. [#] V.I. Lebedev, and D.N. Laikov
"A quadrature formula for the sphere of the 131st
algebraic order of accuracy"
Doklady Mathematics, Vol. 59, No. 3, 1999, pp. 477-481.
.. [#] https://de.mathworks.com/matlabcentral/fileexchange/27097-\
getlebedevsphere
"""
warnings.warn((
"This function will be deprecated in pyfar 0.8.0 in favor "
"of spharpy.samplings.hyperinterpolation."),
PyfarDeprecationWarning, stacklevel=2)
# possible degrees
degrees = np.array([6, 14, 26, 38, 50, 74, 86, 110, 146, 170, 194, 230,
266, 302, 350, 434, 590, 770, 974, 1202, 1454, 1730,
2030, 2354, 2702, 3074, 3470, 3890, 4334, 4802, 5294,
5810], dtype=int)
# corresponding spherical harmonic orders
orders = np.array((np.floor(np.sqrt(degrees / 1.3) - 1)), dtype=int)
# list possible sh orders and degrees
if n_points is None and sh_order is None:
print('Possible input values:')
for o, d in zip(orders, degrees):
print(f"SH order {o}, number of points {d}")
return None
# check input
if n_points is not None and sh_order is not None:
raise ValueError("Either n_points or sh_order must be None.")
# check if the order is available
if sh_order is not None:
if sh_order not in orders:
str_orders = [f"{o}" for o in orders]
raise ValueError("Invalid spherical harmonic order 'sh_order'. \
Valid orders are: {}.".format(
', '.join(str_orders)))
n_points = int(np.squeeze(degrees[orders == sh_order]))
# check if n_points is available
if n_points not in degrees:
str_degrees = [f"{d}" for d in degrees]
raise ValueError("Invalid number of points n_points. Valid degrees \
are: {}.".format(', '.join(str_degrees)))
# calculate sh_order
sh_order = int(np.squeeze(orders[degrees == n_points]))
# get the samlpling
leb = external.lebedev_sphere(n_points)
# normalize the weights
weights = leb["w"] / (4 * np.pi)
# generate Coordinates object
sampling = pyfar.Coordinates(
leb["x"] * radius,
leb["y"] * radius,
leb["z"] * radius,
sh_order=sh_order, weights=weights,
comment='spherical Lebedev sampling grid')
return sampling
[docs]
def sph_fliege(n_points=None, sh_order=None, radius=1.):
"""
Return Fliege-Maier spherical sampling grid.
This function will be deprecated in pyfar 0.8.0 in favor
of :py:func:`spharpy.samplings.fliege`.
For detailed information, see [#]_. Call :py:func:`sph_fliege`
for a list of possible values for `n_points` and `sh_order`.
Parameters
----------
n_points : int, optional
Number of sampling points in the grid. Related to the spherical
harmonic order by ``n_points = (sh_order + 1)**2``. Either `n_points`
or `sh_order` must be provided. The default is ``None``.
sh_order : int, optional
Maximum applicable spherical harmonic order. Related to the number of
points by ``sh_order = np.sqrt(n_points) - 1``. Either `n_points` or
`sh_order` must be provided. The default is ``None``.
radius : number, optional
Radius of the sampling grid in meters. The default is ``1``.
Returns
-------
sampling : Coordinates
Sampling positions including sampling weights.
Notes
-----
This implementation uses pre-calculated points from the SOFiA
toolbox [#]_. Possible combinations of `n_points` and `sh_order` are:
+------------+------------+
| `n_points` | `sh_order` |
+============+============+
| 4 | 1 |
+------------+------------+
| 9 | 2 |
+------------+------------+
| 16 | 3 |
+------------+------------+
| 25 | 4 |
+------------+------------+
| 36 | 5 |
+------------+------------+
| 49 | 6 |
+------------+------------+
| 64 | 7 |
+------------+------------+
| 81 | 8 |
+------------+------------+
| 100 | 9 |
+------------+------------+
| 121 | 10 |
+------------+------------+
| 144 | 11 |
+------------+------------+
| 169 | 12 |
+------------+------------+
| 196 | 13 |
+------------+------------+
| 225 | 14 |
+------------+------------+
| 256 | 15 |
+------------+------------+
| 289 | 16 |
+------------+------------+
| 324 | 17 |
+------------+------------+
| 361 | 18 |
+------------+------------+
| 400 | 19 |
+------------+------------+
| 441 | 20 |
+------------+------------+
| 484 | 21 |
+------------+------------+
| 529 | 22 |
+------------+------------+
| 576 | 23 |
+------------+------------+
| 625 | 24 |
+------------+------------+
| 676 | 25 |
+------------+------------+
| 729 | 26 |
+------------+------------+
| 784 | 27 |
+------------+------------+
| 841 | 28 |
+------------+------------+
| 900 | 29 |
+------------+------------+
References
----------
.. [#] J. Fliege and U. Maier, "The distribution of points on the sphere
and corresponding cubature formulae,” IMA J. Numerical Analysis,
Vol. 19, pp. 317–334, Apr. 1999, doi: 10.1093/imanum/19.2.317.
.. [#] https://audiogroup.web.th-koeln.de/SOFiA_wiki/DOWNLOAD.html
"""
warnings.warn((
"This function will be deprecated in pyfar 0.8.0 in favor "
"of spharpy.samplings.fliege."),
PyfarDeprecationWarning, stacklevel=2)
# possible values for n_points and sh_order
points = np.array([4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196,
225, 256, 289, 324, 361, 400, 441, 484, 529, 576, 625,
676, 729, 784, 841, 900], dtype=int)
orders = np.array(np.floor(np.sqrt(points) - 1), dtype=int)
# list possible sh orders and number of points
if n_points is None and sh_order is None:
for o, d in zip(orders, points):
print(f"SH order {o}, number of points {d}")
return None
# check input
if n_points is not None and sh_order is not None:
raise ValueError("Either n_points or sh_order must be None.")
if sh_order is not None:
# check if the order is available
if sh_order not in orders:
str_orders = [f"{o}" for o in orders]
raise ValueError("Invalid spherical harmonic order 'sh_order'. \
Valid orders are: {}.".format(
', '.join(str_orders)))
# assign n_points
n_points = int(np.squeeze(points[orders == sh_order]))
else:
# check if n_points is available
if n_points not in points:
str_points = [f"{d}" for d in points]
raise ValueError("Invalid number of points n_points. Valid points \
are: {}.".format(', '.join(str_points)))
# assign sh_order
sh_order = int(np.squeeze(orders[points == n_points]))
# get the sampling points
fliege = sio.loadmat(os.path.join(
os.path.dirname(__file__), "external", "samplings_fliege.mat"),
variable_names=f"Fliege_{int(n_points)}")
fliege = fliege[f"Fliege_{int(n_points)}"]
# generate Coordinates object
sampling = pyfar.Coordinates(
fliege[:, 0],
fliege[:, 1],
radius,
'sph', 'top_colat',
sh_order=sh_order, weights=fliege[:, 2],
comment='spherical Fliege sampling grid')
# switch and invert Coordinates in Cartesian representation to be
# consistent with [1]
xyz = sampling.cartesian
sampling.x = xyz[:, 1]
sampling.y = xyz[:, 0]
sampling.z = -xyz[:, 2]
return sampling
[docs]
def sph_equal_area(n_points, radius=1.):
"""
Sampling based on partitioning into faces with equal area.
This function will be deprecated in pyfar 0.8.0 in favor
of :py:func:`spharpy.samplings.equal_area`.
For detailed information, see [#]_.
Parameters
----------
n_points : int
Number of points corresponding to the number of partitions of the
sphere.
radius : number, optional
Radius of the sampling grid in meters. The default is ``1``.
Returns
-------
sampling : Coordinates
Sampling positions. Sampling weights can be obtained from
:py:func:`calculate_sph_voronoi_weights`.
References
----------
.. [#] 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.
"""
warnings.warn((
"This function will be deprecated in pyfar 0.8.0 in favor "
"of spharpy.samplings.equal_area."),
PyfarDeprecationWarning, stacklevel=2)
point_set = external.eq_point_set(2, n_points)
sampling = pyfar.Coordinates(
point_set[0] * radius, point_set[1] * radius, point_set[2] * radius,
comment='Equal area partitioning of the sphere.')
return sampling
def _sph_extremal_load_data(orders='all'):
"""Download extremal sampling grids.
orders = 'all' : load all samplings up to SH order 99
orders = int, list : load sampling of specified SH order(s)
"""
# set the SH orders to be read
if isinstance(orders, int):
orders = [orders]
elif isinstance(orders, str):
orders = range(1, 100)
elif not isinstance(orders, list):
raise ValueError("orders must an int, list, or string.")
print("Loading extremal sampling points from \
https://web.maths.unsw.edu.au/~rsw/Sphere/MaxDet/. \
This might take a while but is only done once.")
http = urllib3.PoolManager(cert_reqs=False)
prefix = 'samplings_extremal_'
for sh_order in orders:
# number of sampling points
n_points = (sh_order + 1)**2
# load the data
filename = "md%03d.%05d" % (sh_order, n_points)
url = "https://web.maths.unsw.edu.au/~rsw/Sphere/S2Pts/MD/"
fileurl = url + filename
# Kontrolle ist gut, Vertrauen ist besser
with warnings.catch_warnings():
warnings.simplefilter("ignore", InsecureRequestWarning)
http_data = http.urlopen('GET', fileurl)
# save the data
if http_data.status == 200:
save_name = os.path.join(
os.path.dirname(__file__), "external", prefix + filename)
print(f'Loading file {sh_order}/{len(orders)}')
with open(save_name, 'wb') as out:
out.write(http_data.data)
else:
raise ConnectionError(
"Connection error. Please check your internet connection.")
def _sph_t_design_load_data(degrees='all'):
"""Download t-design sampling grids.
degrees = 'all' : load all samplings up to degree 99
degrees = number : load sampling of specified degree
"""
# set the degrees to be read
if isinstance(degrees, int):
degrees = [degrees]
elif isinstance(degrees, str):
degrees = range(1, 100)
elif not isinstance(degrees, list):
raise ValueError("degrees must an int, list, or string.")
print("Loading t-design sampling points from \
http://web.maths.unsw.edu.au/~rsw/Sphere/EffSphDes/sf.html. \
This might take a while but is only done once.")
http = urllib3.PoolManager(cert_reqs=False)
prefix = 'samplings_t_design_'
n_points_exceptions = {3: 8, 5: 18, 7: 32, 9: 50, 11: 72, 13: 98, 15: 128}
for degree in degrees:
# number of sampling points
n_points = int(np.ceil((degree + 1)**2 / 2) + 1)
if degree in n_points_exceptions:
n_points = n_points_exceptions[degree]
# load the data
filename = "sf%03d.%05d" % (degree, n_points)
url = "http://web.maths.unsw.edu.au/~rsw/Sphere/Points/SF/"\
"SF29-Nov-2012/"
fileurl = url + filename
# Kontrolle ist gut, Vertrauen ist besser
with warnings.catch_warnings():
warnings.simplefilter("ignore", InsecureRequestWarning)
http_data = http.urlopen('GET', fileurl)
# save the data
if http_data.status == 200:
save_name = os.path.join(
os.path.dirname(__file__), "external", prefix + filename)
print(f'Loading file {degree}/{len(degrees)}')
with open(save_name, 'wb') as out:
out.write(http_data.data)
else:
raise ConnectionError(
"Connection error. Please check your internet connection.")