r"""
The SamplingSphere class inherits from the :py:mod:`pyfar.classes.coordinates`
class, which supports various coordinate systems and the conversion between
them.
:py:class:`~spharpy.SamplingSphere` is designed to represent a set of
points on a sphere.
Therefore, all points must have the same radius within an absolute tolerance,
defined by :py:attr:`~spharpy.SamplingSphere.radius_tolerance`. If the
:py:attr:`~spharpy.SamplingSphere.weights` are not None, their sum must
equal the integral over the unit sphere, which is :math:`4\pi`.
The property :py:attr:`~spharpy.SamplingSphere.quadrature` is relevant for
spherical harmonic processing. It specifies if the points form a
quadrature rule, which requires valid weights, that the maximum
spherical harmonic order of the sampling
grid :py:attr:`~spharpy.SamplingSphere.n_max` is specified, and the inner
product of the weighted spherical harmonics matrix :math:`\mathrm{Y}`
yields the identity matrix
:math:`\mathrm{Y}^\mathrm{T} \text{diag}\{w\}\mathrm{Y}=\mathrm{I}`,
with the weights vector :math:`w`. The sampling is considered a valid
quadrature if the maximum absolute deviation of
:math:`\mathrm{Y}^\mathrm{T} \text{diag}\{w\}\mathrm{Y}` from :math:`I` is
smaller than the specified
:py:attr:`~spharpy.SamplingSphere.quadrature_tolerance`.
It also adds the additional property:
- :py:attr:`~spharpy.SamplingSphere.n_max`: the maximum spherical harmonic
order of the sampling grid.
Note that the :py:mod:`spharpy.samplings` module provides a set of
predefined spherical sampling grids, which can be used to create a
:py:class:`spharpy.SamplingSphere` object.
"""
import numpy as np
from pyfar.classes.coordinates import sph2cart, cyl2cart
import pyfar as pf
from spharpy.spherical import spherical_harmonic_basis_real
[docs]
class SamplingSphere(pf.Coordinates):
"""Class for samplings on a sphere."""
def __init__(
self, x=None, y=None, z=None, n_max=None, weights: np.array = None,
comment: str = "", radius_tolerance=1e-6,
quadrature_tolerance=1e-10):
r"""
Create a SamplingSphere class object from a set of points on a sphere.
See :py:mod:`pyfar.classes.coordinates` for more information.
Parameters
----------
x : ndarray, number
X coordinate of a right handed Cartesian coordinate system in
meters (-\infty < x < \infty).
y : ndarray, number
Y coordinate of a right handed Cartesian coordinate system in
meters (-\infty < y < \infty).
z : ndarray, number
Z coordinate of a right handed Cartesian coordinate system in
meters (-\infty < z < \infty).
n_max : int, optional
Maximum spherical harmonic order of the sampling grid. If
provided, it must be an integer greater or equal to 0.
The default is ``None``.
weights: array like, number, optional
Weighting factors for coordinate points. Their sum must equal to
the integral over the unit sphere, which is :math:`4\pi`.
The `shape` of the array must match the `shape` of the individual
coordinate arrays. The default is ``None``, which means that no
weights are used.
comment : str, optional
Comment about the stored coordinate points. The default is
``""``, which initializes an empty string.
radius_tolerance : float, optional
All points that are stored in a SamplingSphere must have the same
radius and an error is raised if the maximum deviation from the
mean radius exceeds this tolerance. The default of ``1e-6`` meter
is intended to allow for some numerical inaccuracy.
quadrature_tolerance : float, optional
Tolerance for testing whether the provided sampling grid is
a valid quadrature. The sampling is considered a valid quadrature
if the maximum absolute deviation of the inner product of the
weighted spherical harmonics matrix from the identity matrix is
smaller than the specified tolerance. The default is ``1e-10``.
"""
self._radius_tolerance = None
self.radius_tolerance = radius_tolerance
pf.Coordinates.__init__(
self, x, y, z, weights=weights, comment=comment)
self._n_max = n_max
self._quadrature_tolerance = None
self.quadrature_tolerance = quadrature_tolerance
self._quadrature = None
[docs]
@classmethod
def from_coordinates(
cls, coordinates, n_max=None, radius_tolerance: float = 1e-6,
quadrature_tolerance: float = 1e-10):
r"""
Convert Coordinates class object to SamplingSphere class object.
Parameters
----------
coordinates : pyfar.Coordinates
Coordinates to be converted.
n_max : int, optional
Maximum spherical harmonic order of the sampling grid.
The default is ``None``.
radius_tolerance : float, optional
All points that are stored in a SamplingSphere must have the same
radius and an error is raised if the maximum deviation from the
mean radius exceeds this tolerance. The default of ``1e-6`` meter
is intended to allow for some numerical inaccuracy.
quadrature_tolerance : float, optional
Tolerance for testing whether the provided sampling grid is
a valid quadrature. The sampling is considered a valid quadrature
if the maximum absolute deviation of the inner product of the
weighted spherical harmonics matrix from the identity matrix is
smaller than the specified tolerance. The default is ``1e-10``.
"""
if type(coordinates) is not pf.Coordinates:
raise TypeError('coordinates must be a pyfar Coordinates object')
# make sure mutable data is copied
if coordinates.weights is None:
weights = None
else:
weights = coordinates.weights.copy()
return cls(
coordinates.x, coordinates.y, coordinates.z,
weights=weights, comment=coordinates.comment,
n_max=n_max, radius_tolerance=radius_tolerance,
quadrature_tolerance=quadrature_tolerance)
[docs]
@classmethod
def from_cartesian(
cls, x, y, z, n_max=None, weights: np.array = None,
comment: str = "", radius_tolerance: float = 1e-6,
quadrature_tolerance: float = 1e-10):
r"""
Create a SamplingSphere class object from a set of points on a sphere.
See :py:mod:`pyfar.classes.coordinates` for more information.
Parameters
----------
x : ndarray, number
X coordinate of a right handed Cartesian coordinate system in
meters (-\infty < x < \infty).
y : ndarray, number
Y coordinate of a right handed Cartesian coordinate system in
meters (-\infty < y < \infty).
z : ndarray, number
Z coordinate of a right handed Cartesian coordinate system in
meters (-\infty < z < \infty).
n_max : int, optional
Maximum spherical harmonic order of the sampling grid.
The default is ``None``.
weights: array like, number, optional
Weighting factors for coordinate points. Their sum must equal to
the integral over the unit sphere, which is :math:`4\pi`.
The `shape` of the array must match the `shape` of the individual
coordinate arrays. The default is ``None``, which means that no
weights are used.
comment : str, optional
Comment about the stored coordinate points. The default is
``""``, which initializes an empty string.
radius_tolerance : float, optional
All points that are stored in a SamplingSphere must have the same
radius and an error is raised if the maximum deviation from the
mean radius exceeds this tolerance. The default of ``1e-6`` meter
is intended to allow for some numerical inaccuracy.
quadrature_tolerance : float, optional
Tolerance for testing whether the provided sampling grid is
a valid quadrature. The sampling is considered a valid quadrature
if the maximum absolute deviation of the inner product of the
weighted spherical harmonics matrix from the identity matrix is
smaller than the specified tolerance. The default is ``1e-10``.
Examples
--------
Create a SamplingSphere object
>>> import spharpy
>>> sampling = spharpy.SamplingSphere.from_cartesian(0, 0, 1)
or the same using
>>> import spharpy
>>> sampling = spharpy.SamplingSphere(0, 0, 1)
"""
return cls(
x, y, z, weights=weights, comment=comment, n_max=n_max,
radius_tolerance=radius_tolerance,
quadrature_tolerance=quadrature_tolerance)
[docs]
@classmethod
def from_spherical_elevation(
cls, azimuth, elevation, radius, n_max=None,
weights: np.array = None, comment: str = "",
radius_tolerance: float = 1e-6,
quadrature_tolerance: float = 1e-10):
r"""
Create a SamplingSphere class object from a set of points on a sphere.
See :py:mod:`pyfar.classes.coordinates` for more information.
Parameters
----------
azimuth : ndarray, double
Angle in radiant of rotation from the x-y-plane facing towards
positive x direction. Used for spherical and cylindrical coordinate
systems.
elevation : ndarray, double
Angle in radiant with respect to horizontal plane (x-z-plane).
Used for spherical coordinate systems.
radius : ndarray, double
Distance to origin for each point. Used for spherical coordinate
systems.
n_max : int, optional
Maximum spherical harmonic order of the sampling grid.
The default is ``None``.
weights: array like, float, None, optional
Weighting factors for coordinate points. Their sum must equal to
the integral over the unit sphere, which is :math:`4\pi`.
The `shape` of the array must match the `shape` of the individual
coordinate arrays. The default is ``None``, which means that no
weights are used.
comment : str, optional
Comment about the stored coordinate points. The default is
``""``, which initializes an empty string.
radius_tolerance : float, optional
All points that are stored in a SamplingSphere must have the same
radius and an error is raised if the maximum deviation from the
mean radius exceeds this tolerance. The default of ``1e-6`` meter
is intended to allow for some numerical inaccuracy.
quadrature_tolerance : float, optional
Tolerance for testing whether the provided sampling grid is
a valid quadrature. The sampling is considered a valid quadrature
if the maximum absolute deviation of the inner product of the
weighted spherical harmonics matrix from the identity matrix is
smaller than the specified tolerance. The default is ``1e-10``.
Examples
--------
Create a SamplingSphere object
>>> import spharpy
>>> sampling = spharpy.SamplingSphere.from_spherical_elevation(0, 0, 1)
"""
x, y, z = sph2cart(
azimuth, np.pi / 2 - np.atleast_1d(elevation), radius)
return cls(
x, y, z, weights=weights, comment=comment, n_max=n_max,
radius_tolerance=radius_tolerance,
quadrature_tolerance=quadrature_tolerance)
[docs]
@classmethod
def from_spherical_colatitude(
cls, azimuth, colatitude, radius, n_max=None,
weights: np.array = None, comment: str = "",
radius_tolerance: float = 1e-6,
quadrature_tolerance: float = 1e-10):
r"""
Create a SamplingSphere class object from a set of points on a sphere.
See :py:mod:`pyfar.classes.coordinates` for more information.
Parameters
----------
azimuth : ndarray, double
Angle in radiant of rotation from the x-y-plane facing towards
positive x direction. Used for spherical and cylindrical coordinate
systems.
colatitude : ndarray, double
Angle in radiant with respect to polar axis (z-axis). Used for
spherical coordinate systems.
radius : ndarray, double
Distance to origin for each point. Used for spherical coordinate
systems.
n_max : int, optional
Maximum spherical harmonic order of the sampling grid.
The default is ``None``.
weights: array like, number, optional
Weighting factors for coordinate points. Their sum must equal to
the integral over the unit sphere, which is :math:`4\pi`.
The `shape` of the array must match the `shape` of the individual
coordinate arrays. The default is ``None``, which means that no
weights are used.
comment : str, optional
Comment about the stored coordinate points. The default is
``""``, which initializes an empty string.
radius_tolerance : float, optional
All points that are stored in a SamplingSphere must have the same
radius and an error is raised if the maximum deviation from the
mean radius exceeds this tolerance. The default of ``1e-6`` meter
is intended to allow for some numerical inaccuracy.
quadrature_tolerance : float, optional
Tolerance for testing whether the provided sampling grid is
a valid quadrature. The sampling is considered a valid quadrature
if the maximum absolute deviation of the inner product of the
weighted spherical harmonics matrix from the identity matrix is
smaller than the specified tolerance. The default is ``1e-10``.
Examples
--------
Create a SamplingSphere object
>>> import spharpy
>>> sampling = spharpy.SamplingSphere.from_spherical_colatitude(
... 0, 0, 1)
"""
x, y, z = sph2cart(azimuth, colatitude, radius)
return cls(
x, y, z, weights=weights, comment=comment, n_max=n_max,
radius_tolerance=radius_tolerance,
quadrature_tolerance=quadrature_tolerance)
[docs]
@classmethod
def from_spherical_side(
cls, lateral, polar, radius, n_max=None,
weights: np.array = None, comment: str = "",
radius_tolerance: float = 1e-6,
quadrature_tolerance: float = 1e-10):
r"""
Create a SamplingSphere class object from a set of points on a sphere.
See :py:mod:`pyfar.classes.coordinates` for more information.
Parameters
----------
lateral : ndarray, double
Angle in radiant with respect to horizontal plane (x-y-plane).
Used for spherical coordinate systems.
polar : ndarray, double
Angle in radiant of rotation from the x-z-plane facing towards
positive x direction. Used for spherical coordinate systems.
radius : ndarray, double
Distance to origin for each point. Used for spherical coordinate
systems.
n_max : int, optional
Maximum spherical harmonic order of the sampling grid.
The default is ``None``.
weights: array like, number, optional
Weighting factors for coordinate points. Their sum must equal to
the integral over the unit sphere, which is :math:`4\pi`.
The `shape` of the array must match the `shape` of the individual
coordinate arrays. The default is ``None``, which means that no
weights are used.
comment : str, optional
Comment about the stored coordinate points. The default is
``""``, which initializes an empty string.
radius_tolerance : float, optional
All points that are stored in a SamplingSphere must have the same
radius and an error is raised if the maximum deviation from the
mean radius exceeds this tolerance. The default of ``1e-6`` meter
is intended to allow for some numerical inaccuracy.
quadrature_tolerance : float, optional
Tolerance for testing whether the provided sampling grid is
a valid quadrature. The sampling is considered a valid quadrature
if the maximum absolute deviation of the inner product of the
weighted spherical harmonics matrix from the identity matrix is
smaller than the specified tolerance. The default is ``1e-10``.
Examples
--------
Create a SamplingSphere object
>>> import spharpy
>>> sampling = spharpy.SamplingSphere.from_spherical_side(0, 0, 1)
"""
x, z, y = sph2cart(
polar, np.pi / 2 - np.atleast_1d(lateral), radius)
return cls(
x, y, z, weights=weights, comment=comment, n_max=n_max,
radius_tolerance=radius_tolerance,
quadrature_tolerance=quadrature_tolerance)
[docs]
@classmethod
def from_spherical_front(
cls, frontal, upper, radius, n_max=None, weights: np.array = None,
comment: str = "", radius_tolerance: float = 1e-6,
quadrature_tolerance: float = 1e-10):
r"""
Create a SamplingSphere class object from a set of points on a sphere.
See :py:mod:`pyfar.classes.coordinates` for more information.
Parameters
----------
frontal : ndarray, double
Angle in radiant of rotation from the y-z-plane facing towards
positive y direction. Used for spherical coordinate systems.
upper : ndarray, double
Angle in radiant with respect to polar axis (x-axis). Used for
spherical coordinate systems.
radius : ndarray, double
Distance to origin for each point. Used for spherical coordinate
systems.
n_max : int, optional
Maximum spherical harmonic order of the sampling grid.
The default is ``None``.
weights: array like, number, optional
Weighting factors for coordinate points. Their sum must equal to
the integral over the unit sphere, which is :math:`4\pi`.
The `shape` of the array must match the `shape` of the individual
coordinate arrays. The default is ``None``, which means that no
weights are used.
comment : str, optional
Comment about the stored coordinate points. The default is
``""``, which initializes an empty string.
radius_tolerance : float, optional
All points that are stored in a SamplingSphere must have the same
radius and an error is raised if the maximum deviation from the
mean radius exceeds this tolerance. The default of ``1e-6`` meter
is intended to allow for some numerical inaccuracy.
quadrature_tolerance : float, optional
Tolerance for testing whether the provided sampling grid is
a valid quadrature. The sampling is considered a valid quadrature
if the maximum absolute deviation of the inner product of the
weighted spherical harmonics matrix from the identity matrix is
smaller than the specified tolerance. The default is ``1e-10``.
Examples
--------
Create a SamplingSphere object
>>> import spharpy
>>> sampling = spharpy.SamplingSphere.from_spherical_front(0, 0, 1)
"""
y, z, x = sph2cart(frontal, upper, radius)
return cls(
x, y, z, weights=weights, comment=comment, n_max=n_max,
radius_tolerance=radius_tolerance,
quadrature_tolerance=quadrature_tolerance)
[docs]
@classmethod
def from_cylindrical(
cls, azimuth, z, rho, n_max=None, weights: np.array = None,
comment: str = "", radius_tolerance: float = 1e-6,
quadrature_tolerance: float = 1e-10):
r"""
Create a SamplingSphere class object from a set of points on a sphere.
See :py:mod:`pyfar.classes.coordinates` for more information.
Parameters
----------
azimuth : ndarray, double
Angle in radiant of rotation from the x-y-plane facing towards
positive x direction. Used for spherical and cylindrical coordinate
systems.
z : ndarray, double
The z coordinate
rho : ndarray, double
Distance to origin for each point in the x-y-plane. Used for
cylindrical coordinate systems.
n_max : int, optional
Maximum spherical harmonic order of the sampling grid.
The default is ``None``.
weights: array like, number, optional
Weighting factors for coordinate points. Their sum must equal to
the integral over the unit sphere, which is :math:`4\pi`.
The `shape` of the array must match the `shape` of the individual
coordinate arrays. The default is ``None``, which means that no
weights are used.
comment : str, optional
Comment about the stored coordinate points. The default is
``""``, which initializes an empty string.
radius_tolerance : float, optional
All points that are stored in a SamplingSphere must have the same
radius and an error is raised if the maximum deviation from the
mean radius exceeds this tolerance. The default of ``1e-6`` meter
is intended to allow for some numerical inaccuracy.
quadrature_tolerance : float, optional
Tolerance for testing whether the provided sampling grid is
a valid quadrature. The sampling is considered a valid quadrature
if the maximum absolute deviation of the inner product of the
weighted spherical harmonics matrix from the identity matrix is
smaller than the specified tolerance. The default is ``1e-10``.
Examples
--------
Create a SamplingSphere object
>>> import spharpy
>>> sampling = spharpy.SamplingSphere.from_cylindrical(
... 0, 0, 1, sh_order=1)
"""
x, y, z = cyl2cart(azimuth, z, rho)
return cls(
x, y, z, weights=weights, comment=comment, n_max=n_max,
radius_tolerance=radius_tolerance,
quadrature_tolerance=quadrature_tolerance)
@property
def n_max(self):
"""Get or set the spherical harmonic order."""
return self._n_max
@n_max.setter
def n_max(self, value):
"""Get or set the spherical harmonic order."""
assert value >= 0
if value is None:
self._n_max = None
self._quadrature = False
else:
if self._n_max != value:
self._quadrature = None
self._n_max = int(value)
@property
def radius_tolerance(self):
"""Get or set the radius tolerance in meter."""
return self._radius_tolerance
@radius_tolerance.setter
def radius_tolerance(self, value):
"""Get or set the radius tolerance in meter."""
# check input
if not isinstance(value, (int, float)) or value <= 0:
raise ValueError(
'The radius tolerance must be a number greater than zero')
current_tolerance = self.radius_tolerance
self._radius_tolerance = float(value)
# Check if points meet new tolerance if points exist
if hasattr(self, 'x'):
try:
self._check_points(self._x, self._y, self._z)
except ValueError as e:
# revert setting the tolerance and raise the error
self._radius_tolerance = current_tolerance
raise e
@property
def quadrature_tolerance(self):
"""Get or set the quadrature tolerance."""
return self._quadrature_tolerance
@quadrature_tolerance.setter
def quadrature_tolerance(self, value):
"""Get or set the quadrature tolerance."""
# check input
if not isinstance(value, (int, float)) or value <= 0:
raise ValueError(
'The quadrature tolerance must be a number greater than zero')
self._quadrature_tolerance = float(value)
self._quadrature = None
def _check_points(self, x, y, z):
"""Check input data before setting coordinates."""
# convert to numpy arrays of the same shape
x, y, z = super()._check_points(x, y, z)
# check for equal radius
radius = np.sqrt(x.flatten()**2 + y.flatten()**2 + z.flatten()**2)
radius_delta = np.max(radius) - np.min(radius)
if radius_delta > self.radius_tolerance:
raise ValueError(
'All points must have the same radius but the difference '
f'between the minimum and maximum radius is {radius_delta:.3g}'
' m, which exceeds the tolerance of '
f'{self.radius_tolerance:.3g} m. The tolerance can be changed '
'using SamplingSphere.radius_tolerance.')
# reset the quadrature flag to make sure it is checked again after
# adding or changing points in the SamplingSphere
self._quadrature = None
return x, y, z
def _check_weights(self, weights):
r"""Check if the weights are valid.
The weights must be positive and their sum must equal integration of
the unit sphere, i.e. :math:`4\pi`.
Parameters
----------
weights : array like, number
the weights for each point, should be of size of self.csize.
Returns
-------
weights : np.ndarray[float64], None
The weights reshaped to the cshape of the coordinates if not None.
Otherwise None.
"""
weights = super()._check_weights(weights)
if weights is None:
return weights
if np.any(weights < 0) or np.any(np.isnan(weights)):
raise ValueError("All weights must be positive numeric values.")
if not np.isclose(np.sum(weights), 4*np.pi, atol=1e-6, rtol=1e-6):
raise ValueError(
"The sum of the weights must be equal to 4*pi. "
f"Current sum: {np.sum(weights)}")
return weights
def _check_quadrature(self):
r"""Check if the sampling is a valid quadrature.
Returns
-------
check : bool
Indicates if sampling is a valid quadrature
"""
if self.n_max is None or self.weights is None:
return False
# create basis matrix
sh_basis = spherical_harmonic_basis_real(self.n_max, self)
# test if basis is quadrature
quad_evaluation = sh_basis.T @ np.diag(self.weights) @ sh_basis
identity = np.eye((self.n_max + 1)**2)
error = np.max(np.abs(quad_evaluation-identity))
return error < self.quadrature_tolerance
@property
def weights(self):
r"""The area/quadrature weights of the sampling.
Their sum must equal to :math:`4\pi`.
"""
return super().weights
@weights.setter
def weights(self, weights):
r"""Get or set the area/quadrature weights of the sampling.
Their sum must equal to :math:`4\pi`.
"""
if not np.array_equal(weights, self.weights):
self._quadrature = None
super(__class__, type(self)).weights.fset(self, weights)
@property
def quadrature(self):
"""Get the quadrature flag."""
if self._quadrature is None:
# recompute _quadrature flag
self._quadrature = self._check_quadrature()
return self._quadrature
def __repr__(self):
"""Get info about SamplingSphere object."""
if self.csize == 0:
return 'SamplingSphere: empty'
return f'SamplingSphere: n_max={self.n_max}, cshape={self.cshape}'