Source code for spharpy.special

"""
Module implementing or wrapping special functions required for higher level
classes and functions of the spharpy package.
"""

from itertools import count
import numpy as np
import scipy.special as _spspecial
from scipy.optimize import brentq


[docs] def spherical_bessel(n, z, derivative=False): r""" Spherical Bessel function of the first kind and of order n evaluated at z. The spherical Bessel function of the first kind is defined as ([#]_, Eq. 2.29) .. math:: j_n(z) = \sqrt{\frac{\pi}{2z}} J_{n+\frac{1}{2}} (z) with the Bessel function of the first kind :math:`J_\alpha`. Parameters ---------- n : int, ndarray Order of the spherical Bessel function z : double, ndarray Argument of the spherical Bessel function. Has to be real valued. derivative : bool Return the derivative of the spherical Bessel function. Default is ``False``. Returns ------- jn : double, ndarray Spherical Bessel function. Array with dimensions [N x Z], where N is the number of elements in n and Z is the number of elements in z. Note ---- This is a wrapper around the Scipy implementation of the :py:func:`spherical Bessel function <scipy.special.spherical_jn>`. References ---------- .. [#] Rafaely, B. (2019). Fundamentals of spherical array processing (2nd ed.). Springer. https://doi.org/10.1007/978-3-319-99561-8 """ ufunc = _spspecial.spherical_jn n = np.asarray(n, dtype=int) z = np.asarray(z, dtype=float) bessel = np.zeros((n.size, z.size), dtype=complex) if n.size > 1: for idx, order in zip(count(), n): bessel[idx, :] = ufunc(order, z, derivative=derivative) else: bessel = ufunc(n, z, derivative=derivative) if z.ndim <= 1 or n.ndim <= 1: bessel = np.squeeze(bessel) return bessel
[docs] def spherical_bessel_zeros(n_max, n_zeros): r"""Compute the zeros of the spherical Bessel function. This function will always start at order zero which is equal to :math:`\sin(z)/z` and iteratively compute the roots for higher orders. The roots are computed using Brents algorithm from the scipy package. See :py:func:`~spherical_bessel` for more information. Parameters ---------- n_max : int The order of the spherical bessel function n_zeros : int The number of roots to be computed Returns ------- roots : ndarray, double The roots of the spherical bessel function """ def func(x, n): return _spspecial.spherical_jn(n, x) zerosj = np.zeros((n_max+1, n_zeros), dtype=float) zerosj[0] = np.arange(1, n_zeros+1)*np.pi points = np.arange(1, n_zeros+n_max+1)*np.pi roots = np.zeros(n_zeros+n_max, dtype=float) for i in range(1, n_max+1): for j in range(n_zeros+n_max-i): roots[j] = brentq(func, points[j], points[j+1], (i,), maxiter=5000) points = roots zerosj[i, :n_zeros] = roots[:n_zeros] return zerosj
[docs] def spherical_hankel(n, z, kind=2, derivative=False): r""" Spherical Hankel function of the first kind and order n evaluated at z. The spherical Hankel function of the first kind is defined as ([#]_, Eq. 2.30) .. math:: h_n(z) = \sqrt{\frac{\pi}{2z}} H_{n+\frac{1}{2}} (z) with the Hankel function of the first kind :math:`H_\alpha`. Parameters ---------- n : int, ndarray Order of the spherical bessel function z : double, ndarray Argument of the spherical bessel function. Has to be real valued. kind : int, optional The kind of the spherical Hankel function. Must be ``1`` or ``2``. The default is ``2``. derivative : bool, optional Flag to indicate if the derivative os the spherical Hankel function should be returned. The default is ``False``. Returns ------- hn : double, ndarray Spherical bessel function. Array with dimensions [N x Z], where N is the number of elements in n and Z is the number of elements in z. References ---------- .. [#] Rafaely, B. (2019). Fundamentals of spherical array processing (2nd ed.). Springer. https://doi.org/10.1007/978-3-319-99561-8 Note ---- This is based on the Hankel functions implemented in the scipy package. """ if kind not in (1, 2): raise ValueError("The spherical hankel function can \ only be of first or second kind.") n = np.asarray(n, dtype=int) z = np.asarray(z, dtype=float) ufunc = _spherical_hankel_derivative if derivative else _spherical_hankel if n.size > 1: hankel = np.zeros((n.size, z.size), dtype=complex) for idx, order in zip(count(), n): hankel[idx, :] = ufunc(order, z, kind) else: hankel = ufunc(n, z, kind) if z.ndim <= 1 or n.ndim <= 1: hankel = np.squeeze(hankel) return hankel
def _spherical_hankel(n, z, kind): if kind == 1: hankel = _spspecial.hankel1(n+0.5, z) elif kind == 2: hankel = _spspecial.hankel2(n+0.5, z) hankel = np.sqrt(np.pi/2/z) * hankel return hankel def _spherical_hankel_derivative(n, z, kind): return _spherical_hankel(n - 1, z, kind) - (n + 1) / z * _spherical_hankel( n, z, kind)
[docs] def spherical_harmonic(n, m, theta, phi): """The spherical harmonics of order n and degree m. The spherical harmonic functions are fully normalized (N3D) and include the Condon-Shortley phase according to [#]_. n : unsigned int The spherical harmonic order m : int The spherical harmonic degree theta : ndarray, double The colatitude angle phi : ndarray, double The azimuth angle Returns ------- spherical_harmonic : ndarray, double The complex valued spherical harmonic of order n and degree m Note ---- This function wraps the spherical harmonic implementation from scipy. References ---------- .. [#] B. Rafaely, "Fundamentals of Spherical Array Processing", (2015), Springer-Verlag """ return _spspecial.sph_harm_y(n, m, theta, phi)
[docs] def spherical_harmonic_real(n, m, theta, phi): r"""Real valued spherical harmonic function of order n and degree m evaluated at the angles theta and phi. The spherical harmonic functions are fully normalized (N3D) and follow the AmbiX phase convention, which does not include the Condon-Shortley phase [#]_. .. math:: Y_n^m(\theta, \phi) = \sqrt{\frac{2n+1}{4\pi} \frac{(n-|m|)!}{(n+|m|)!}} P_n^{|m|}(\cos \theta) \begin{cases} \displaystyle \cos(|m|\phi), & \text{if $m \ge 0$} \newline \displaystyle \sin(|m|\phi) , & \text{if $m < 0$} \end{cases} References ---------- .. [#] C. Nachbar, F. Zotter, E. Deleflie, and A. Sontacchi, “Ambix - A Suggested Ambisonics Format (revised by F. Zotter),” International Symposium on Ambisonics and Spherical Acoustics, vol. 3, pp. 1–11, 2011. Parameters ---------- n : unsigned int The spherical harmonic order m : int The spherical harmonic degree theta : ndarray, double The colatitude angle phi : ndarray, double The azimuth angle Returns ------- spherical_harmonic : ndarray, double The real valued spherial harmonic of order n and degree m """ # careful here, scipy uses phi as the elevation angle and # theta as the azimuth angle Y_nm_cplx = spherical_harmonic(n, m, theta, phi) if m == 0: Y_nm = np.real(Y_nm_cplx) elif m > 0: Y_nm = np.real(Y_nm_cplx) * np.sqrt(2) elif m < 0: Y_nm = np.imag(Y_nm_cplx) * np.sqrt(2) * float(-1)**(m+1) Y_nm *= float(-1)**(m) return Y_nm
[docs] def spherical_harmonic_derivative_phi(n, m, theta, phi): """Calculate the derivative of the spherical harmonics with respect to the azimuth angle phi. Parameters ---------- n : int Spherical harmonic order m : int Spherical harmonic degree theta : double Elevation angle 0 < theta < pi phi : double Azimuth angle 0 < phi < 2*pi Returns ------- sh_diff : complex double Spherical harmonic derivative """ if m == 0 or n == 0: res = np.zeros(phi.shape, dtype=complex) else: res = spherical_harmonic(n, m, theta, phi) * 1j * m return res
[docs] def spherical_harmonic_gradient_phi(n, m, theta, phi): """Calculate the derivative of the spherical harmonics with respect to the azimuth angle phi divided by sin(theta). Parameters ---------- n : int Spherical harmonic order m : int Spherical harmonic degree theta : double Elevation angle 0 < theta < pi phi : double Azimuth angle 0 < phi < 2*pi Returns ------- sh_diff : complex double Spherical harmonic derivative """ if m == 0: return np.zeros(theta.shape, dtype=complex) factor = np.sqrt((2*n+1)/(2*n-1))/2 exp_phi = np.exp(1j*phi) first = np.sqrt((n+m)*(n+m-1)) * exp_phi * spherical_harmonic( n-1, m-1, theta, phi) second = np.sqrt((n-m) * (n-m-1)) / exp_phi * spherical_harmonic( n-1, m+1, theta, phi) Ynm_sin_theta = (-1) * factor * (first + second) return Ynm_sin_theta * 1j
[docs] def spherical_harmonic_derivative_theta(n, m, theta, phi): """Calculate the derivative of the spherical harmonics with respect to the elevation angle theta. Parameters ---------- n : int Spherical harmonic order m : int Spherical harmonic degree theta : double Elevation angle 0 < theta < pi phi : double Azimuth angle 0 < phi < 2*pi Returns ------- sh_diff : complex double Spherical harmonic derivative """ if n == 0: return np.zeros(theta.shape, dtype=complex) exp_phi = np.exp(1j*phi) first = np.sqrt((n-m+1) * (n+m)) * exp_phi * spherical_harmonic( n, m-1, theta, phi) second = np.sqrt((n-m) * (n+m+1)) / exp_phi * spherical_harmonic( n, m+1, theta, phi) return (first-second)/2 * (-1)
[docs] def legendre_function(n, m, z, cs_phase=True): r""" Legendre function of order n and degree m with argument z. The Legendre function is defined as ([#]_ Eq. 1.30, 1.33, and 1.34) .. math:: P_n^m(z) = (-1)^m(1-z^2)^{m/2}\frac{d^m}{dz^m}P_n(z), where the Condon-Shortley phase term :math:`(-1)^m` is dropped when ``cs_phase = False`` and the Legendre Polynomial :math:`P_n(z)`. Parameters ---------- n : int The order m : int The degree z : ndarray, double The argument as an array cs_phase : bool, optional Whether to include the Condon-Shortley phase term :math:`(-1)^m` or not. Default is ``True``. Returns ------- legendre : ndarray, double The Legendre function. This will return zeros if :math:`|m| > n`. References ---------- .. [#] Rafaely, B. (2019). Fundamentals of spherical array processing (2nd ed.). Springer. https://doi.org/10.1007/978-3-319-99561-8 Note ---- This is a wrapper for the Legendre function implementation from scipy. The scipy implementation uses the Condon-Shortley phase. Therefore, the sign needs to be flipped here for uneven degrees when dropping the Condon-Shortley phase. """ z = np.atleast_1d(z) # squeeze required because the legendre function introduced in scipy 1.15 # returns a 2D array, whereas the previous function `lpmn` returned a 1D # array legendre = np.squeeze(_spspecial.assoc_legendre_p(n, m, z)) # remove Condon-Shortley phase if not cs_phase and m % 2: legendre *= -1 return legendre
[docs] def spherical_harmonic_normalization(n, m, norm='full'): r"""The normalization factor for real valued spherical harmonics. .. math:: N_n^m = \sqrt{\frac{2n+1}{4\pi}\frac{(n-m)!}{(n+m)!}} Parameters ---------- n : int The spherical harmonic order. m : int The spherical harmonic degree. norm : 'full', 'semi', optional Normalization to use. Can be either fully normalized on the sphere or semi-normalized. Default is ``'full'``. Returns ------- norm : double The normalization factor. """ if np.abs(m) > n: factor = 0.0 elif norm == 'full': z = n+m+1 factor = _spspecial.poch(z, -2*m) factor *= (2*n+1)/(4*np.pi) if int(m) != 0: factor *= 2 factor = np.sqrt(factor) elif norm == 'semi': z = n+m+1 factor = _spspecial.poch(z, -2*m) if int(m) != 0: factor *= 2 factor = np.sqrt(factor) else: raise ValueError("Unknown normalization.") return factor
[docs] def spherical_harmonic_derivative_theta_real(n, m, theta, phi): r"""The derivative of the real valued spherical harmonics with respect to the elevation angle :math:`\theta`. Parameters ---------- n : int The spherical harmonic order. m : int The spherical harmonic degree. theta : ndarray, double The elevation angle phi : ndarray, double The azimuth angle Returns ------- derivative : ndarray, double The derivative Note ---- This implementation does not include the Condon-Shortley phase term. """ m_abs = np.abs(m) if n == 0: return np.zeros(theta.shape, dtype=float) first = (n+m_abs)*(n-m_abs+1) * \ legendre_function( n, m_abs-1, np.cos(theta), cs_phase=False) second = legendre_function( n, m_abs+1, np.cos(theta), cs_phase=False) legendre_diff = 0.5*(first - second) N_nm = spherical_harmonic_normalization(n, m_abs) phi_term = np.sin(m_abs*phi) if m < 0 else np.cos(m_abs*phi) return N_nm * legendre_diff * phi_term
[docs] def spherical_harmonic_derivative_phi_real(n, m, theta, phi): r"""The derivative of the real valued spherical harmonics with respect to the azimuth angle :math:`\phi`. Parameters ---------- n : int The spherical harmonic order. m : int The spherical harmonic degree. theta : ndarray, double The elevation angle phi : ndarray, double The azimuth angle Returns ------- derivative : ndarray, double The derivative Note ---- This implementation does not include the Condon-Shortley phase term. """ m_abs = np.abs(m) if m == 0: return np.zeros(theta.shape, dtype=float) legendre = legendre_function(n, m_abs, np.cos(theta), cs_phase=False) N_nm = spherical_harmonic_normalization(n, m_abs) if m < 0: phi_term = np.cos(m_abs*phi) * m_abs else: phi_term = -np.sin(m_abs*phi) * m_abs return N_nm * legendre * phi_term
[docs] def spherical_harmonic_gradient_phi_real(n, m, theta, phi): r"""The gradient of the real valued spherical harmonics with respect to the azimuth angle :math:`\phi`. The singularities at the poles are avoided using the formulation proposed in [#]_. Parameters ---------- n : int The spherical harmonic order. m : int The spherical harmonic degree. theta : ndarray, double The elevation angle phi : ndarray, double The azimuth angle Returns ------- derivative : ndarray, double The derivative Note ---- This implementation does not include the Condon-Shortley phase term. References ---------- .. [#] J. Du, C. Chen, V. Lesur, and L. Wang, “Non-singular spherical harmonic expressions of geomagnetic vector and gradient tensor fields in the local north-oriented reference frame,” Geoscientific Model Development, vol. 8, no. 7, pp. 1979–1990, Jul. 2015. """ m_abs = np.abs(m) if m == 0: return np.zeros(theta.shape, dtype=float) first = (n+m_abs)*(n+m_abs-1) * \ legendre_function( n-1, m_abs-1, np.cos(theta), cs_phase=False) second = legendre_function( n-1, m_abs+1, np.cos(theta), cs_phase=False) legendre_diff = 0.5*(first + second) N_nm = spherical_harmonic_normalization(n, m_abs) phi_term = np.cos(m_abs*phi) if m < 0 else -np.sin(m_abs*phi) return N_nm * legendre_diff * phi_term
[docs] def legendre_coefficients(order): """Calculate the coefficients of a Legendre polynomial of the specified order. Parameters ---------- order : int The order of the polynomial Returns ------- coefficients : ndarray, double The coefficients of the polynomial Note ---- This uses :py:mod:`numpy.polynomial.legendre` to compute the coefficients. """ leg = np.polynomial.legendre.Legendre.basis(order) return np.polynomial.legendre.leg2poly(leg.coef)
[docs] def chebyshev_coefficients(order): """Calculate the coefficients of a Chebyshev polynomial of the specified order. Parameters ---------- order : int The order of the polynomial Returns ------- coefficients : ndarray, double The coefficients of the polynomial Note ---- This uses :py:mod:`numpy.polynomial.chebyshev` to compute the coefficients. """ cheb = np.polynomial.chebyshev.Chebyshev.basis(order) return np.polynomial.chebyshev.cheb2poly(cheb.coef)