Source code for parts.scattering.psd.d14

r"""
The Delanoƫ (2014) PSD
======================

The D14 particle size distribution as proposed by Delanoƫ in :cite:`delanoe2014`
uses a normalized form of the modified gamma distribution, parametrized
as follows:

.. math::

    \frac{dN(X)}{dX} = N_0^* \beta \frac{\Gamma(4)}{4^4}
           \frac{\Gamma(\frac{\alpha + 5}{\beta})^{(4 + \alpha)}}
                {\Gamma(\frac{\alpha + 4}{\beta})^{(5 + \alpha)}}
    X^\alpha \exp \left \{- \left (X \frac{\Gamma(\frac{\alpha + 5}{\beta})}
                                        {\Gamma(\frac{\alpha + 4}{\beta})}
                                   \right )^\beta \right \},

The parameter X is defined as  the volume equivalent sphere diameter
:math:`D_{eq}` normalized by the mass-weighted mean diameter:

.. math::

    X = \frac{D_{eq}}{D_m}

The PSD is thus parametrized by four parameters:
    - :math:`N_0^*`, here called the *intercept parameter*
    - :math:`D_m`, the *mass-weighted mean diameter*
    - the shape parameters :math:`\alpha` and :math:`\beta`

Of these, :math:`\alpha` and :math:`\beta` are assumed constant, while
:math:`N_0` and :math:`D_m` are the free parameters that descibe
the particles throughout the atmosphere.

The particle mass density :math:`\rho_m` per bulk volume can be computed
from :math:`N_0` and :math:`D_m` using:

.. math::
    \rho_m = \frac{\Gamma(4)}{4^4}\frac{\pi \rho}{6}N_0^*D_m^4

In this module, two implementations of the D14 PSD are provided:

- the :class:`D14` class that uses the mass-density and :math:`D_m` as
  moments of the PSD
- the :class:`D14N` :class that uses the intercept parameter :math:`N_0^*`
  and :math:`D_m` as moments of the PSD

"""
from parts import dimensions as dim
from parts.scattering.psd.arts.arts_psd import ArtsPSD
from parts.scattering.psd.data.psd_data import PSDData, D_eq
from typhon.arts.workspace import arts_agenda
import numpy as np
import scipy as sp
from scipy.special import gamma

################################################################################
# General PSD function
################################################################################

[docs]def evaluate_d14(x, n0, dm, alpha, beta): """ Compute the particle size distribution of the D14 PSD. Parameters: x(numpy.array): 1D array containing the values of the size parameter :math:`D_{eq}` at which to evaluate the PSD. If :code:`x` is not 1D it will be flattened. n0(numpy.array or scalar): Array containing the values of the intercept parameter for which the PSD should be evaluated. dm(numpy.array or scalar): Array containing the values of the mass weighted mean diameter at which to evaluate the PSD. Must be broadcastable to the shape of :code:`n0` alpha(numpy.array or scalar): Array containing the values of the :math:`alpha` parameter a which to evaulate the PSD. Must be broadcastable to the shape of :code: `n0` beta(numpy.array or scalar): Array containing the values of the :math:`beta` parameter a which to evaulate the PSD. Must be broadcastable to the shape of :code: `n0` Returns: Array :code:`dNdD_eq` containing the computed values of the PSD. The first dimensions of :code:`dNdD_eq` correspond to the shape of the :code:`n0` parameter and the last dimension to the size parameter. """ shape = n0.shape result_shape = shape + (1,) n0 = np.reshape(n0, result_shape) try: dm = np.broadcast_to(dm, shape).reshape(result_shape) except: raise Exception("Could not broadcast 'dm' parameter to shape of 'n0' " "parameter.") try: alpha = np.broadcast_to(alpha, shape).reshape(result_shape) except: raise Exception("Could not broadcast 'alpha' parameter to shape of 'n0' " "parameter.") try: beta = np.broadcast_to(beta, shape).reshape(result_shape) except: raise Exception("Could not broadcast 'beta' parameter to shape of 'n0' " "parameter.") x = x.reshape((1,) * len(shape) + (-1,)) x = x / dm c1 = gamma(4.0) / 4 ** 4 c2 = gamma((alpha + 5) / beta) ** (4 + alpha) / \ gamma((alpha + 4) / beta) ** (5 + alpha) c3 = gamma((alpha + 5) / beta) / \ gamma((alpha + 4) / beta) y = n0 * beta * c1 * c2 y = y * x ** alpha y *= np.exp(- (x * c3) ** beta) # Set invalid values to zero y[np.broadcast_to(dm == 0.0, y.shape)] = 0.0 return y
################################################################################ # PSD classes ################################################################################
[docs]class D14(ArtsPSD): """ Implementation of the D14 PSD that uses mass density :math:`\rho_m` and the mass-weighted mean diamter :math:`D_m` as free parameters. """
[docs] @classmethod def from_psd_data(self, psd, alpha, beta, rho): """ Create an instance of the D14 PSD from existing PSD data. Parameters: :code:`psd`: A numeric or analytic representation of a PSD. alpha(:code:`numpy.ndarray`): The :math:`alpha` parameter of the to use for the D14 PSD. beta(:code:`numpy.ndarray`): The :math:`beta` parameter of the to use for the D14 PSD. rho(:code:`numpy.float`): The density to use for the D14 PSD """ new_psd = D14(alpha, beta, rho) new_psd.convert_from(psd) return new_psd
def convert_from(self, psd): md = psd.get_mass_density() m4 = psd.get_moment(4.0, reference_size_parameter = self.size_parameter) m3 = psd.get_moment(3.0, reference_size_parameter = self.size_parameter) dm = m4 / m3 dm[m3 == 0.0] = 0.0 self.mass_density = md self.mass_weighted_diameter = dm def __init__(self, alpha, beta, rho = 917.0, mass_density = None, mass_weighted_diameter = None): """ Parameters: alpha(numpy.float): The value of the :math:`alpha` parameter for the PSD beta(numpy.float): The value of the :math:`beta` parameter for the PSD rho(numpy.float): The particle density to use for the conversion to mass density. mass_density(numpy.array): If provided, this can be used to fix the value of the mass density which will then not be queried from the data provider. mass_weighted_diameter(numpy.array): If provided, this can be used to fix the value of the mass weighted mean diameter which will then not be queried from the data provider. """ from parts.scattering.psd.data.psd_data import D_eq self.alpha = alpha self.beta = beta self.rho = rho if not mass_density is None: self.mass_density = mass_density if not mass_weighted_diameter is None: self.mass_weighted_diameter = mass_weighted_diameter super().__init__(D_eq(self.rho)) self.rho = rho self.dm_min = 1e-12 @property def moment_names(self): return ["mass_density", "mass_weighted_diameter"] @property def moments(self): return [self.mass_density, self.mass_weighted_diameter] @property def pnd_call_agenda(self): @arts_agenda def pnd_call(ws): ws.psdD14(n0Star = -999.0, Dm = np.nan, iwc = np.nan, rho = self.rho, alpha = self.alpha, beta = self.beta, t_min = self.t_min, Dm_min = self.dm_min, t_max = self.t_max) return pnd_call def _get_parameters(self): md = self.mass_density if md is None: raise Exception("The 'mass_density' array needs to be set to use" "this function.") shape = md.shape dm = self.mass_weighted_diameter if dm is None: raise Exception("The 'mass_weighted_diameter' array needs to be set " "to use this function.") try: dm = np.broadcast_to(dm, shape) except: raise Exception("Could not broadcast the 'mass_weighted_diameter'" "data into the shape of the mass density data.") try: alpha = np.broadcast_to(self.alpha, shape) except: raise Exception("Could not broadcast the data for the 'alpha' " " parameter into the shape the mass density data.") try: beta = np.broadcast_to(self.beta, shape) except: raise Exception("Could not broadcast the data for the 'beta' " " parameter into the shape the mass density data.") return md, dm, alpha, beta
[docs] def get_moment(self, p, reference_size_parameter = None): """ Computes the moments of the PSD analytically. Parameters: p(:code:`numpy.float`): Wich moment of the PSD to compute reference_size_parameter(:class:`SizeParameter`): Size parameter with respect to which the moment should be computed. Returns: Array containing the :math:`p` th moment of the PSD. """ if not reference_size_parameter is None: a1 = self.size_parameter.a b1 = self.size_parameter.b a2 = reference_size_parameter.a b2 = reference_size_parameter.b c = (a1 / a2) ** (p / b2) p = p * b1 / b2 else: c = 1.0 md, dm, alpha, beta = self._get_parameters() n0 = 4.0 ** 4 / (np.pi * self.rho) * md / dm ** 4.0 nu_mgd = beta lmbd_mgd = gamma((alpha + 5) / beta) / \ gamma((alpha + 4) / beta) alpha_mgd = (alpha + 1) / beta - 1 n_mgd = n0 * gamma(4.0) / 4.0 ** 4 * \ gamma((alpha + 1) / beta) * \ gamma((alpha + 5) / beta) ** 3 / \ gamma((alpha + 4) / beta) ** 4 m = n_mgd / lmbd_mgd ** p m *= gamma(1 + alpha_mgd + p / nu_mgd) m /= gamma(1 + alpha_mgd) return c * m * dm ** (p + 1)
[docs] def get_mass_density(self): """ Returns: Array containing the mass density for all the bulk volumes described by this PSD. """ if self.mass_density is None: raise Exception("The free mass_density parameter has not been set.") else: return self.mass_density
[docs] def evaluate(self, x): """ Compute value of the particle size distribution for given values of the size parameter. Parameters: x(numpy.array): Array containing the values of :math:`D_eq` at which to compute the number density. Returns: Array :code:`dNdD_eq` containing the computed values of the PSD. The first dimensions of :code:`dNdD_eq` correspond to the shape of the :code:`n0` parameter and the last dimension to the size parameter. """ try: md = self.mass_density except: raise Exception("The 'mass_density' array needs to be set, before" " the PSD can be evaluated.") try: dm = self.mass_weighted_diameter except: raise Exception("The 'mass_weighted_diameter' array needs to be" " set, before the PSD can be evaluated.") n0 = 4.0 ** 4 / (np.pi * self.rho) * md / dm ** 4.0 y = evaluate_d14(x, n0, dm, self.alpha, self.beta) return PSDData(x, y, D_eq(self.rho))
[docs]class D14N(ArtsPSD): """ Implementation of the D14 PSD that uses the intercept parameter :math:`N_0^*` and the mass-weighted mean diamter :math:`D_m` as free parameters. """
[docs] @classmethod def from_psd_data(cls, psd, alpha, beta, rho): """ Create an instance of the D14 PSD from existing PSD data. Parameters: :code:`psd`: A numeric or analytic representation of a PSD. alpha(:code:`numpy.ndarray`): The :math:`alpha` parameter of the to use for the D14 PSD. beta(:code:`numpy.ndarray`): The :math:`beta` parameter of the to use for the D14 PSD. rho(:code:`numpy.float`): The density to use for the D14 PSD """ new_psd = cls(alpha, beta, rho) new_psd.convert_from(psd) return new_psd
def convert_from(self, psd): md = psd.get_mass_density() m4 = psd.get_moment(4.0, reference_size_parameter = self.size_parameter) m3 = psd.get_moment(3.0, reference_size_parameter = self.size_parameter) dm = m4 / m3 dm[m3 == 0.0] = 0.0 n0 = 4.0 ** 4 / (np.pi * self.rho) * md / dm ** 4 n0[m3 == 0.0] = 0.0 self.mass_density = md self.intercept_parameter = n0 self.mass_weighted_diameter = dm def __init__(self, alpha, beta, rho = 917.0, intercept_parameter = None, mass_weighted_diameter = None): """ Parameters: alpha(numpy.float): The value of the :math:`alpha` parameter for the PSD beta(numpy.float): The value of the :math:`beta` parameter for the PSD rho(numpy.float): The particle density to use for the conversion to mass density. intercept_parameter(numpy.array): If provided, this can be used to fix the value of the mass density which will then not be queried from the data provider. mass_weighted_diameter(numpy.array): If provided, this can be used to fix the value of the mass weighted mean diameter which will then not be queried from the data provider. """ from parts.scattering.psd.data.psd_data import D_eq self.alpha = alpha self.beta = beta self.rho = rho if not intercept_parameter is None: self.intercept_parameter = intercept_parameter if not mass_weighted_diameter is None: self.mass_weighted_diameter = mass_weighted_diameter self.dm_min = 1e-12 super().__init__(D_eq(self.rho)) @property def moment_names(self): return ["intercept_parameter", "mass_weighted_diameter"] @property def moments(self): return [self.intercept_parameter, self.mass_weighted_diameter] @property def pnd_call_agenda(self): @arts_agenda def pnd_call(ws): ws.psdD14(n0Star = np.nan, Dm = np.nan, iwc = -999.0, rho = self.rho, alpha = self.alpha, beta = self.beta, t_min = self.t_min, Dm_min = self.dm_min, t_max = self.t_max) return pnd_call def _get_parameters(self): n0 = self.intercept_parameter if n0 is None: raise Exception("The 'intercept_parameter' data needs to be set to " " use this function.") shape = n0.shape dm = self.mass_weighted_diameter if dm is None: raise Exception("The 'mass_weighted_diameter' array needs to be set " "to use this function.") try: dm = np.broadcast_to(dm, shape) except: raise Exception("Could not broadcast the 'mass_weighted_diameter'" "data into the shape of the mass density data.") try: alpha = np.broadcast_to(self.alpha, shape) except: raise Exception("Could not broadcast the data for the 'alpha' " " parameter into the shape the mass density data.") try: beta = np.broadcast_to(self.beta, shape) except: raise Exception("Could not broadcast the data for the 'beta' " " parameter into the shape the mass density data.") return n0, dm, alpha, beta
[docs] def get_mass_density(self): """ Returns: Array containing the mass density for all the bulk volumes described by this PSD. """ if self.intercept_parameter is None \ or self.mass_weighted_diameter is None : raise Exception("The parameters of the PSD have not been set.") else: c = gamma(4.0) / 4.0 ** 4.0 m = c * np.pi * self.rho / 6.0 * self.intercept_parameter \ * self.mass_weighted_diameter ** 4.0 return m
[docs] def get_moment(self, p, reference_size_parameter = None): """ Computes the moments of the PSD analytically. The physical significance of a moment of a PSD depends on the size parameter. So in general, the moments of the same PSD given w.r.t. different size parameters differ. If the :code:`reference_size_parameter` argument is given then the computed moment will correspond to the Moment of the PSD w.r.t. to the given size parameter. Parameters: p(:code:`numpy.float`): Wich moment of the PSD to compute reference_size_parameter(SizeParameter): Size parameter with respect to which the moment should be computed. Returns: Array containing the :math:`p` th moment of the PSD. """ if not reference_size_parameter is None: a1 = self.size_parameter.a b1 = self.size_parameter.b a2 = reference_size_parameter.a b2 = reference_size_parameter.b c = (a1 / a2) ** (p / b2) p = p * b1 / b2 else: c = 1.0 n0, dm, alpha, beta = self._get_parameters() nu_mgd = beta lmbd_mgd = gamma((alpha + 5) / beta) / \ gamma((alpha + 4) / beta) alpha_mgd = (alpha + 1) / beta - 1 n_mgd = n0 * gamma(4.0) / 4.0 ** 4 * \ gamma((alpha + 1) / beta) * \ gamma((alpha + 5) / beta) ** 3 / \ gamma((alpha + 4) / beta) ** 4 m = n_mgd / lmbd_mgd ** p m *= gamma(1 + alpha_mgd + p / nu_mgd) m /= gamma(1 + alpha_mgd) return c * m * dm ** (p + 1)
[docs] def evaluate(self, x): """ Compute value of the particle size distribution for given values of the size parameter. Parameters: x(numpy.array): Array containing the values of :math:`D_eq` at which to compute the number density. Returns: Array :code:`dNdD_eq` containing the computed values of the PSD. The first dimensions of :code:`dNdD_eq` correspond to the shape of the :code:`n0` parameter and the last dimension to the size parameter. """ n0 = self.intercept_parameter if n0 is None: raise Exception("The 'intercept_parameter' array needs to be set, before" " the PSD can be evaluated.") dm = self.mass_weighted_diameter if dm is None: raise Exception("The 'mass_weighted_diameter' array needs to be" " set, before the PSD can be evaluated.") y = evaluate_d14(x, n0, dm, self.alpha, self.beta) return PSDData(x, y, D_eq(self.rho))
[docs]class D14MN(D14N): """ Implementation of the D14 PSD that uses mass density $m$ and intercept parameter :math:`N_0^*` as free parameters. """ def __init__(self, alpha, beta, rho = 917.0, mass_density = None, intercept_parameter = None): """ Parameters: alpha(numpy.float): The value of the :math:`alpha` parameter for the PSD beta(numpy.float): The value of the :math:`beta` parameter for the PSD rho(numpy.float): The particle density to use for the conversion to mass density. mass_density(numpy.array): If provided, this can be used to fix the mass density which will then not be queried from the data provider. intercept_parameter(numpy.array): If provided, this can be used to fix the value of the intercept parameter $N_0^*$ which will then not be queried from the data provider. """ from parts.scattering.psd.data.psd_data import D_eq if (not mass_density is None) and (not intercept_parameter is None): self.mass_density = mass_density dm = (4.0 ** 4 / np.pi / rho * mass_density / intercept_parameter) ** (1 / 4.0) else: dm = None super().__init__(alpha, beta, rho, intercept_parameter, dm) @property def moment_names(self): return ["mass_density", "intercept_parameter"] @property def moments(self): return [self.mass_density, self.intercept_parameter] @property def pnd_call_agenda(self): @arts_agenda def pnd_call(ws): ws.psdD14(n0Star = np.nan, Dm = -999.0, iwc = np.nan, rho = self.rho, alpha = self.alpha, beta = self.beta, t_min = self.t_min, Dm_min = self.dm_min, t_max = self.t_max) return pnd_call def _get_parameters(self): md = self.mass_density if md is None: raise Exception("The 'intercept_parameter' data needs to be set to " " use this function.") shape = md.shape n0 = self.intercept_parameter if n0 is None: raise Exception("The 'intercept_parameter' data needs to be set to " " use this function.") dm = (4.0 ** 4 / np.pi / self.rho * md / n0) ** 0.25 try: alpha = np.broadcast_to(self.alpha, shape) except: raise Exception("Could not broadcast the data for the 'alpha' " " parameter into the shape the mass density data.") try: beta = np.broadcast_to(self.beta, shape) except: raise Exception("Could not broadcast the data for the 'beta' " " parameter into the shape the mass density data.") return n0, dm, alpha, beta
[docs] def get_mass_density(self): """ Returns: Array containing the mass density for all the bulk volumes described by this PSD. """ return self.mass_density
[docs] def evaluate(self, x): """ Compute value of the particle size distribution for given values of the size parameter. Parameters: x(numpy.array): Array containing the values of :math:`D_eq` at which to compute the number density. Returns: Array :code:`dNdD_eq` containing the computed values of the PSD. The first dimensions of :code:`dNdD_eq` correspond to the shape of the :code:`n0` parameter and the last dimension to the size parameter. """ n0, dm, alpha, beta = self._get_parameters() y = evaluate_d14(x, n0, dm, alpha, beta) return PSDData(x, y, D_eq(self.rho))