Source code for pynbody.sph.kernels

"""SPH kernel details"""

from __future__ import annotations

import numpy as np
import scipy.integrate as integrate


[docs] class KernelBase: """Base class for SPH kernels""" _sample_cache = {}
[docs] def __init__(self): self.h_power = 3 # Return the power of the smoothing length which appears in # the denominator of the expression for the general kernel. # Will be 3 for 3D kernels, 2 for 2D kernels. self.max_d = 2
# The maximum value of the displacement over the smoothing for # which the kernel is non-zero def _get_samples_from_cache(self): if hash(self) in KernelBase._sample_cache: return KernelBase._sample_cache[hash(self)] else: return None def get_samples(self, dtype=np.float32): import time s = time.time() samples = self._get_samples_from_cache() if samples is None: sample_pts = np.arange(0, 4.01, 0.02) samples = np.array([self.get_value(x ** 0.5) for x in sample_pts], dtype=dtype) KernelBase._sample_cache[hash(self)] = samples return samples
[docs] def get_value(self, d, h=1) -> float: """Get the value of the kernel for a given smoothing length. Here ``d`` is the separation *in units of the smoothing length*, i.e. ``d = r/h``. For a vectorised version taking the separation directly, see :meth:`value`. """ raise NotImplementedError("Subclasses must implement this method")
[docs] def value(self, r, h): r"""Get the kernel :math:`W(r, h)`, vectorised over ``r`` and ``h``. .. versionadded:: 2.6.0 Unlike :meth:`get_value`, this takes the separation ``r`` directly rather than ``r/h``, and both arguments may be arrays. This is the form needed when evaluating a kernel for a list of particle pairs, e.g. inside a callback passed to :meth:`pynbody.kdtree.KDTree.pair_reduce`. Parameters ---------- r : array_like Separation between particles. h : array_like Smoothing length. The kernel has support out to ``r = 2h``. Returns ------- numpy.ndarray The kernel value, normalised such that :math:`4\pi \int W(r, h) r^2 dr = 1`. """ raise NotImplementedError("Subclasses must implement this method")
[docs] def gradient(self, r, h): r"""Get :math:`dW/dr`, vectorised over ``r`` and ``h``. .. versionadded:: 2.6.0 This is the radial derivative of :meth:`value`, and is negative on :math:`0 < r < 2h`. The full gradient with respect to the position of particle :math:`i` is .. math:: \nabla_i W(|x_i - x_j|, h) = \frac{dW}{dr} \frac{x_i - x_j}{r} Parameters ---------- r : array_like Separation between particles. h : array_like Smoothing length. Returns ------- numpy.ndarray The radial derivative of the kernel. """ raise NotImplementedError("Subclasses must implement this method")
@staticmethod def _as_arrays(r, h): return (np.asarray(r, dtype=np.float64), np.asarray(h, dtype=np.float64))
[docs] def projection(self) -> KernelBase: """Return a 2D projection of this kernel""" return Kernel2D(self)
def __hash__(self): return hash(self.__class__)
[docs] @classmethod def get_c_kernel_id(cls) -> int: """Return the C kernel id for this kernel This is used to select the appropriate C code for the kernel, and must match the kernel id defined in the Kernel::create function in kernels.hpp""" raise NotImplementedError("Subclasses must implement this method")
[docs] class CubicSplineKernel(KernelBase): """A cubic spline kernel. This is the default kernel used by pynbody."""
[docs] def get_value(self, d, h=1): if d < 1: f = 1. - (3. / 2) * d ** 2 + (3. / 4.) * d ** 3 elif d < 2: f = 0.25 * (2. - d) ** 3 else: f = 0 return f / (np.pi * h ** 3)
[docs] def value(self, r, h): r, h = self._as_arrays(r, h) q = r / h f = np.where(q < 1.0, 1.0 - 1.5 * q ** 2 + 0.75 * q ** 3, 0.25 * np.clip(2.0 - q, 0.0, None) ** 3) return f / (np.pi * h ** 3)
[docs] def gradient(self, r, h): r, h = self._as_arrays(r, h) q = r / h df = np.where(q < 1.0, -3.0 * q + 2.25 * q ** 2, -0.75 * np.clip(2.0 - q, 0.0, None) ** 2) return df / (np.pi * h ** 4)
[docs] @classmethod def get_c_kernel_id(cls): return 0
[docs] class WendlandC2Kernel(KernelBase): """A Wendland C2 (quintic) kernel. This is the default kernel used by EAGLE."""
[docs] def get_value(self, d, h=1): if d < 2: f = (1. - (d / 2.))**4 * (2. * d + 1) else: f = 0 return (21. * f) / (16. * np.pi * h ** 3)
[docs] def value(self, r, h): r, h = self._as_arrays(r, h) q = r / h t = np.clip(1.0 - 0.5 * q, 0.0, None) return 21.0 * t ** 4 * (2.0 * q + 1.0) / (16.0 * np.pi * h ** 3)
[docs] def gradient(self, r, h): # d/dq [ (1-q/2)^4 (2q+1) ] = -5 q (1-q/2)^3 r, h = self._as_arrays(r, h) q = r / h t = np.clip(1.0 - 0.5 * q, 0.0, None) return -21.0 * 5.0 * q * t ** 3 / (16.0 * np.pi * h ** 4)
[docs] @classmethod def get_c_kernel_id(cls): return 1
[docs] class Kernel2D(KernelBase): """A 2D spline kernel, generated by numerically projecting an underlying 3D kernel"""
[docs] def __init__(self, k_orig=CubicSplineKernel()): """Create a 2D kernel by projecting a 3D kernel. The 3D kernel is passed as an argument.""" self.h_power = 2 self.max_d = k_orig.max_d self.k_orig = k_orig
[docs] def projection(self): raise ValueError("Cannot project a 2D kernel")
[docs] def get_value(self, d, h=1): return 2 * integrate.quad(lambda z: self.k_orig.get_value(np.sqrt(z ** 2 + d ** 2), h), 0, 2*h)[0]
[docs] def get_c_kernel_id(self): raise NotImplementedError("2D kernels are not supported in C")
def __hash__(self): return hash((self.__class__, self.k_orig))
[docs] def create_kernel_from_c_id(kernel_id: int) -> KernelBase: """Create a kernel object from the integer id used by the C++ code. .. versionadded:: 2.6.0 This is the inverse of :meth:`KernelBase.get_c_kernel_id`, and is used to recover a python-side kernel from a :class:`pynbody.kdtree.KDTree` whose kernel was set from a serialized state. """ for subclass in KernelBase.__subclasses__(): try: if subclass.get_c_kernel_id() == kernel_id: return subclass() except (NotImplementedError, TypeError): # e.g. Kernel2D, which has no C counterpart continue raise ValueError("No kernel corresponds to C kernel id %r" % kernel_id)
[docs] def create_kernel(spec) -> KernelBase: """Create a kernel object from a string specification, a type, an existing kernel object, or a None This function is used to create a kernel object from a variety of input types. It is used by the framework to allow the user flexibility in specifying the kernel type. If the input is a string, it is assumed to be the name of a kernel class, and an object of that class is created. You can use the name of the class with or without the 'Kernel' suffix, and the case is ignored. For example, 'WendlandC2Kernel', 'wendlandc2', and 'WendlandC2' all return a WendlandC2Kernel instance. If the input is a subclass of KernelBase, it is assumed to be a kernel object, and is returned as is. If the input is None, a default kernel is created and returned. Returns ------- KernelBase A kernel object """ if spec is None: from ..configuration import config return create_kernel(config['sph'].get('kernel', 'CubicSplineKernel')) elif isinstance(spec, type): return spec() elif isinstance(spec, KernelBase): return spec elif isinstance(spec, str): for subclass in KernelBase.__subclasses__(): subclass_name = subclass.__name__ if (subclass_name.lower() == spec.lower() or subclass_name.endswith('Kernel') and subclass_name[:-6].lower() == spec.lower()): return subclass() else: raise ValueError("Unknown kernel '%s'" % spec) else: raise ValueError("Unknown kernel specification %r" % spec)