import collections import numbers import numpy as np from ._input_validation import _nonneg_int_or_fail from ._special_ufuncs import (legendre_p, assoc_legendre_p, sph_legendre_p, sph_harm_y) from ._gufuncs import (legendre_p_all, assoc_legendre_p_all, sph_legendre_p_all, sph_harm_y_all) __all__ = [ "assoc_legendre_p", "assoc_legendre_p_all", "legendre_p", "legendre_p_all", "sph_harm_y", "sph_harm_y_all", "sph_legendre_p", "sph_legendre_p_all", ] class MultiUFunc: def __init__(self, ufunc_or_ufuncs, doc=None, *, force_complex_output=False, **default_kwargs): if not isinstance(ufunc_or_ufuncs, np.ufunc): if isinstance(ufunc_or_ufuncs, collections.abc.Mapping): ufuncs_iter = ufunc_or_ufuncs.values() elif isinstance(ufunc_or_ufuncs, collections.abc.Iterable): ufuncs_iter = ufunc_or_ufuncs else: raise ValueError("ufunc_or_ufuncs should be a ufunc or a" " ufunc collection") # Perform input validation to ensure all ufuncs in ufuncs are # actually ufuncs and all take the same input types. seen_input_types = set() for ufunc in ufuncs_iter: if not isinstance(ufunc, np.ufunc): raise ValueError("All ufuncs must have type `numpy.ufunc`." f" Received {ufunc_or_ufuncs}") seen_input_types.add(frozenset(x.split("->")[0] for x in ufunc.types)) if len(seen_input_types) > 1: raise ValueError("All ufuncs must take the same input types.") self._ufunc_or_ufuncs = ufunc_or_ufuncs self.__doc = doc self.__force_complex_output = force_complex_output self._default_kwargs = default_kwargs self._resolve_out_shapes = None self._finalize_out = None self._key = None self._ufunc_default_args = lambda *args, **kwargs: () self._ufunc_default_kwargs = lambda *args, **kwargs: {} @property def __doc__(self): return self.__doc def _override_key(self, func): """Set `key` method by decorating a function. """ self._key = func def _override_ufunc_default_args(self, func): self._ufunc_default_args = func def _override_ufunc_default_kwargs(self, func): self._ufunc_default_kwargs = func def _override_resolve_out_shapes(self, func): """Set `resolve_out_shapes` method by decorating a function.""" if func.__doc__ is None: func.__doc__ = \ """Resolve to output shapes based on relevant inputs.""" func.__name__ = "resolve_out_shapes" self._resolve_out_shapes = func def _override_finalize_out(self, func): self._finalize_out = func def _resolve_ufunc(self, **kwargs): """Resolve to a ufunc based on keyword arguments.""" if isinstance(self._ufunc_or_ufuncs, np.ufunc): return self._ufunc_or_ufuncs ufunc_key = self._key(**kwargs) return self._ufunc_or_ufuncs[ufunc_key] def __call__(self, *args, **kwargs): kwargs = self._default_kwargs | kwargs args += self._ufunc_default_args(**kwargs) ufunc = self._resolve_ufunc(**kwargs) # array arguments to be passed to the ufunc ufunc_args = [np.asarray(arg) for arg in args[-ufunc.nin:]] ufunc_kwargs = self._ufunc_default_kwargs(**kwargs) if (self._resolve_out_shapes is not None): ufunc_arg_shapes = tuple(np.shape(ufunc_arg) for ufunc_arg in ufunc_args) ufunc_out_shapes = self._resolve_out_shapes(*args[:-ufunc.nin], *ufunc_arg_shapes, ufunc.nout, **kwargs) ufunc_arg_dtypes = tuple(ufunc_arg.dtype if hasattr(ufunc_arg, 'dtype') else np.dtype(type(ufunc_arg)) for ufunc_arg in ufunc_args) if hasattr(ufunc, 'resolve_dtypes'): ufunc_dtypes = ufunc_arg_dtypes + ufunc.nout * (None,) ufunc_dtypes = ufunc.resolve_dtypes(ufunc_dtypes) ufunc_out_dtypes = ufunc_dtypes[-ufunc.nout:] else: ufunc_out_dtype = np.result_type(*ufunc_arg_dtypes) if (not np.issubdtype(ufunc_out_dtype, np.inexact)): ufunc_out_dtype = np.float64 ufunc_out_dtypes = ufunc.nout * (ufunc_out_dtype,) if self.__force_complex_output: ufunc_out_dtypes = tuple(np.result_type(1j, ufunc_out_dtype) for ufunc_out_dtype in ufunc_out_dtypes) out = tuple(np.empty(ufunc_out_shape, dtype=ufunc_out_dtype) for ufunc_out_shape, ufunc_out_dtype in zip(ufunc_out_shapes, ufunc_out_dtypes)) ufunc_kwargs['out'] = out out = ufunc(*ufunc_args, **ufunc_kwargs) if (self._finalize_out is not None): out = self._finalize_out(out) return out sph_legendre_p = MultiUFunc( sph_legendre_p, r"""sph_legendre_p(n, m, theta, *, diff_n=0) Spherical Legendre polynomial of the first kind. Parameters ---------- n : ArrayLike[int] Degree of the spherical Legendre polynomial. Must have ``n >= 0``. m : ArrayLike[int] Order of the spherical Legendre polynomial. theta : ArrayLike[float] Input value. diff_n : Optional[int] A non-negative integer. Compute and return all derivatives up to order ``diff_n``. Default is 0. Returns ------- p : ndarray or tuple[ndarray] Spherical Legendre polynomial with ``diff_n`` derivatives. Notes ----- The spherical counterpart of an (unnormalized) associated Legendre polynomial has the additional factor .. math:: \sqrt{\frac{(2 n + 1) (n - m)!}{4 \pi (n + m)!}} It is the same as the spherical harmonic :math:`Y_{n}^{m}(\theta, \phi)` with :math:`\phi = 0`. """, diff_n=0 ) @sph_legendre_p._override_key def _(diff_n): diff_n = _nonneg_int_or_fail(diff_n, "diff_n", strict=False) if not 0 <= diff_n <= 2: raise ValueError( "diff_n is currently only implemented for orders 0, 1, and 2," f" received: {diff_n}." ) return diff_n @sph_legendre_p._override_finalize_out def _(out): return np.moveaxis(out, -1, 0) sph_legendre_p_all = MultiUFunc( sph_legendre_p_all, """sph_legendre_p_all(n, m, theta, *, diff_n=0) All spherical Legendre polynomials of the first kind up to the specified degree ``n`` and order ``m``. Output shape is ``(n + 1, 2 * m + 1, ...)``. The entry at ``(j, i)`` corresponds to degree ``j`` and order ``i`` for all ``0 <= j <= n`` and ``-m <= i <= m``. See Also -------- sph_legendre_p """, diff_n=0 ) @sph_legendre_p_all._override_key def _(diff_n): diff_n = _nonneg_int_or_fail(diff_n, "diff_n", strict=False) if not 0 <= diff_n <= 2: raise ValueError( "diff_n is currently only implemented for orders 0, 1, and 2," f" received: {diff_n}." ) return diff_n @sph_legendre_p_all._override_ufunc_default_kwargs def _(diff_n): return {'axes': [()] + [(0, 1, -1)]} @sph_legendre_p_all._override_resolve_out_shapes def _(n, m, theta_shape, nout, diff_n): if not isinstance(n, numbers.Integral) or (n < 0): raise ValueError("n must be a non-negative integer.") return ((n + 1, 2 * abs(m) + 1) + theta_shape + (diff_n + 1,),) @sph_legendre_p_all._override_finalize_out def _(out): return np.moveaxis(out, -1, 0) assoc_legendre_p = MultiUFunc( assoc_legendre_p, r"""assoc_legendre_p(n, m, z, *, branch_cut=2, norm=False, diff_n=0) Associated Legendre polynomial of the first kind. Parameters ---------- n : ArrayLike[int] Degree of the associated Legendre polynomial. Must have ``n >= 0``. m : ArrayLike[int] order of the associated Legendre polynomial. z : ArrayLike[float | complex] Input value. branch_cut : Optional[ArrayLike[int]] Selects branch cut. Must be 2 (default) or 3. 2: cut on the real axis ``|z| > 1`` 3: cut on the real axis ``-1 < z < 1`` norm : Optional[bool] If ``True``, compute the normalized associated Legendre polynomial. Default is ``False``. diff_n : Optional[int] A non-negative integer. Compute and return all derivatives up to order ``diff_n``. Default is 0. Returns ------- p : ndarray or tuple[ndarray] Associated Legendre polynomial with ``diff_n`` derivatives. Notes ----- The normalized counterpart of an (unnormalized) associated Legendre polynomial has the additional factor .. math:: \sqrt{\frac{(2 n + 1) (n - m)!}{2 (n + m)!}} """, branch_cut=2, norm=False, diff_n=0 ) @assoc_legendre_p._override_key def _(branch_cut, norm, diff_n): diff_n = _nonneg_int_or_fail(diff_n, "diff_n", strict=False) if not 0 <= diff_n <= 2: raise ValueError( "diff_n is currently only implemented for orders 0, 1, and 2," f" received: {diff_n}." ) return norm, diff_n @assoc_legendre_p._override_ufunc_default_args def _(branch_cut, norm, diff_n): return branch_cut, @assoc_legendre_p._override_finalize_out def _(out): return np.moveaxis(out, -1, 0) assoc_legendre_p_all = MultiUFunc( assoc_legendre_p_all, """assoc_legendre_p_all(n, m, z, *, branch_cut=2, norm=False, diff_n=0) All associated Legendre polynomials of the first kind up to the specified degree ``n`` and order ``m``. Output shape is ``(n + 1, 2 * m + 1, ...)``. The entry at ``(j, i)`` corresponds to degree ``j`` and order ``i`` for all ``0 <= j <= n`` and ``-m <= i <= m``. See Also -------- assoc_legendre_p """, branch_cut=2, norm=False, diff_n=0 ) @assoc_legendre_p_all._override_key def _(branch_cut, norm, diff_n): if not ((isinstance(diff_n, numbers.Integral)) and diff_n >= 0): raise ValueError( f"diff_n must be a non-negative integer, received: {diff_n}." ) if not 0 <= diff_n <= 2: raise ValueError( "diff_n is currently only implemented for orders 0, 1, and 2," f" received: {diff_n}." ) return norm, diff_n @assoc_legendre_p_all._override_ufunc_default_args def _(branch_cut, norm, diff_n): return branch_cut, @assoc_legendre_p_all._override_ufunc_default_kwargs def _(branch_cut, norm, diff_n): return {'axes': [(), ()] + [(0, 1, -1)]} @assoc_legendre_p_all._override_resolve_out_shapes def _(n, m, z_shape, branch_cut_shape, nout, **kwargs): diff_n = kwargs['diff_n'] if not isinstance(n, numbers.Integral) or (n < 0): raise ValueError("n must be a non-negative integer.") if not isinstance(m, numbers.Integral) or (m < 0): raise ValueError("m must be a non-negative integer.") return ((n + 1, 2 * abs(m) + 1) + np.broadcast_shapes(z_shape, branch_cut_shape) + (diff_n + 1,),) @assoc_legendre_p_all._override_finalize_out def _(out): return np.moveaxis(out, -1, 0) legendre_p = MultiUFunc( legendre_p, """legendre_p(n, z, *, diff_n=0) Legendre polynomial of the first kind. Parameters ---------- n : ArrayLike[int] Degree of the Legendre polynomial. Must have ``n >= 0``. z : ArrayLike[float] Input value. diff_n : Optional[int] A non-negative integer. Compute and return all derivatives up to order ``diff_n``. Default is 0. Returns ------- p : ndarray or tuple[ndarray] Legendre polynomial with ``diff_n`` derivatives. See Also -------- legendre References ---------- .. [1] Zhang, Shanjie and Jin, Jianming. "Computation of Special Functions", John Wiley and Sons, 1996. https://people.sc.fsu.edu/~jburkardt/f77_src/special_functions/special_functions.html """, diff_n=0 ) @legendre_p._override_key def _(diff_n): if (not isinstance(diff_n, numbers.Integral)) or (diff_n < 0): raise ValueError( f"diff_n must be a non-negative integer, received: {diff_n}." ) if not 0 <= diff_n <= 2: raise NotImplementedError( "diff_n is currently only implemented for orders 0, 1, and 2," f" received: {diff_n}." ) return diff_n @legendre_p._override_finalize_out def _(out): return np.moveaxis(out, -1, 0) legendre_p_all = MultiUFunc( legendre_p_all, """legendre_p_all(n, z, *, diff_n=0) All Legendre polynomials of the first kind up to the specified degree ``n``. Output shape is ``(n + 1, ...)``. The entry at ``j`` corresponds to degree ``j`` for all ``0 <= j <= n``. See Also -------- legendre_p """, diff_n=0 ) @legendre_p_all._override_key def _(diff_n): diff_n = _nonneg_int_or_fail(diff_n, "diff_n", strict=False) if not 0 <= diff_n <= 2: raise ValueError( "diff_n is currently only implemented for orders 0, 1, and 2," f" received: {diff_n}." ) return diff_n @legendre_p_all._override_ufunc_default_kwargs def _(diff_n): return {'axes': [(), (0, -1)]} @legendre_p_all._override_resolve_out_shapes def _(n, z_shape, nout, diff_n): n = _nonneg_int_or_fail(n, 'n', strict=False) return nout * ((n + 1,) + z_shape + (diff_n + 1,),) @legendre_p_all._override_finalize_out def _(out): return np.moveaxis(out, -1, 0) sph_harm_y = MultiUFunc( sph_harm_y, r"""sph_harm_y(n, m, theta, phi, *, diff_n=0) Spherical harmonics. They are defined as .. math:: Y_n^m(\theta,\phi) = \sqrt{\frac{2 n + 1}{4 \pi} \frac{(n - m)!}{(n + m)!}} P_n^m(\cos(\theta)) e^{i m \phi} where :math:`P_n^m` are the (unnormalized) associated Legendre polynomials. Parameters ---------- n : ArrayLike[int] Degree of the harmonic. Must have ``n >= 0``. This is often denoted by ``l`` (lower case L) in descriptions of spherical harmonics. m : ArrayLike[int] Order of the harmonic. theta : ArrayLike[float] Polar (colatitudinal) coordinate; must be in ``[0, pi]``. phi : ArrayLike[float] Azimuthal (longitudinal) coordinate; must be in ``[0, 2*pi]``. diff_n : Optional[int] A non-negative integer. Compute and return all derivatives up to order ``diff_n``. Default is 0. Returns ------- y : ndarray[complex] or tuple[ndarray[complex]] Spherical harmonics with ``diff_n`` derivatives. Notes ----- There are different conventions for the meanings of the input arguments ``theta`` and ``phi``. In SciPy ``theta`` is the polar angle and ``phi`` is the azimuthal angle. It is common to see the opposite convention, that is, ``theta`` as the azimuthal angle and ``phi`` as the polar angle. Note that SciPy's spherical harmonics include the Condon-Shortley phase [2]_ because it is part of `sph_legendre_p`. With SciPy's conventions, the first several spherical harmonics are .. math:: Y_0^0(\theta, \phi) &= \frac{1}{2} \sqrt{\frac{1}{\pi}} \\ Y_1^{-1}(\theta, \phi) &= \frac{1}{2} \sqrt{\frac{3}{2\pi}} e^{-i\phi} \sin(\theta) \\ Y_1^0(\theta, \phi) &= \frac{1}{2} \sqrt{\frac{3}{\pi}} \cos(\theta) \\ Y_1^1(\theta, \phi) &= -\frac{1}{2} \sqrt{\frac{3}{2\pi}} e^{i\phi} \sin(\theta). References ---------- .. [1] Digital Library of Mathematical Functions, 14.30. https://dlmf.nist.gov/14.30 .. [2] https://en.wikipedia.org/wiki/Spherical_harmonics#Condon.E2.80.93Shortley_phase """, force_complex_output=True, diff_n=0 ) @sph_harm_y._override_key def _(diff_n): diff_n = _nonneg_int_or_fail(diff_n, "diff_n", strict=False) if not 0 <= diff_n <= 2: raise ValueError( "diff_n is currently only implemented for orders 0, 1, and 2," f" received: {diff_n}." ) return diff_n @sph_harm_y._override_finalize_out def _(out): if (out.shape[-1] == 1): return out[..., 0, 0] if (out.shape[-1] == 2): return out[..., 0, 0], out[..., [1, 0], [0, 1]] if (out.shape[-1] == 3): return (out[..., 0, 0], out[..., [1, 0], [0, 1]], out[..., [[2, 1], [1, 0]], [[0, 1], [1, 2]]]) sph_harm_y_all = MultiUFunc( sph_harm_y_all, """sph_harm_y_all(n, m, theta, phi, *, diff_n=0) All spherical harmonics up to the specified degree ``n`` and order ``m``. Output shape is ``(n + 1, 2 * m + 1, ...)``. The entry at ``(j, i)`` corresponds to degree ``j`` and order ``i`` for all ``0 <= j <= n`` and ``-m <= i <= m``. See Also -------- sph_harm_y """, force_complex_output=True, diff_n=0 ) @sph_harm_y_all._override_key def _(diff_n): diff_n = _nonneg_int_or_fail(diff_n, "diff_n", strict=False) if not 0 <= diff_n <= 2: raise ValueError( "diff_n is currently only implemented for orders 2," f" received: {diff_n}." ) return diff_n @sph_harm_y_all._override_ufunc_default_kwargs def _(diff_n): return {'axes': [(), ()] + [(0, 1, -2, -1)]} @sph_harm_y_all._override_resolve_out_shapes def _(n, m, theta_shape, phi_shape, nout, **kwargs): diff_n = kwargs['diff_n'] if not isinstance(n, numbers.Integral) or (n < 0): raise ValueError("n must be a non-negative integer.") return ((n + 1, 2 * abs(m) + 1) + np.broadcast_shapes(theta_shape, phi_shape) + (diff_n + 1, diff_n + 1),) @sph_harm_y_all._override_finalize_out def _(out): if (out.shape[-1] == 1): return out[..., 0, 0] if (out.shape[-1] == 2): return out[..., 0, 0], out[..., [1, 0], [0, 1]] if (out.shape[-1] == 3): return (out[..., 0, 0], out[..., [1, 0], [0, 1]], out[..., [[2, 1], [1, 0]], [[0, 1], [1, 2]]])