Sam Chaudry
Upload folder using huggingface_hub
7885a28 verified
raw
history blame
18.5 kB
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]]])