Module diskchef.physics.williams_best

Expand source code
from dataclasses import dataclass
from functools import cached_property

import numpy as np
import pytest
import scipy.integrate
import scipy.interpolate
from astropy import units as u, constants as const

from diskchef.physics.parametrized import ParametrizedPhysics


@dataclass
class WilliamsBest2014(ParametrizedPhysics):
    """
    Class that defines physical model as in Williams & Best 2014
    https://iopscience.iop.org/article/10.1088/0004-637X/788/1/59/pdf

    Usage:

    >>> # Initializing physics class with default parameters
    >>> physics = WilliamsBest2014()
    >>> physics  # doctest: +NORMALIZE_WHITESPACE
    WilliamsBest2014(star_mass=<Quantity 1. solMass>, xray_plasma_temperature=<Quantity 10000000. K>,
                     xray_luminosity=<Quantity 1.e+31 erg / s>, cr_padovani_use_l=False, r_min=<Quantity 0.1 AU>,
                     r_max=<Quantity 500. AU>, zr_max=0.7, radial_bins=100, vertical_bins=100, dust_to_gas=0.01,
                     gas_mass=<Quantity 0.001 solMass>, tapering_radius=<Quantity 100. AU>,
                     tapering_gamma=0.75, midplane_temperature_1au=<Quantity 200. K>,
                      atmosphere_temperature_1au=<Quantity 1000. K>, temperature_slope=0.55,
                      molar_mass=<Quantity 2.33 g / mol>, inner_radius=<Quantity 1. AU>, inner_depletion=1e-06)
    >>> # Defaults can be overridden
    >>> WilliamsBest2014(star_mass=2 * u.solMass, r_max=200 * u.au)  # doctest: +NORMALIZE_WHITESPACE
    WilliamsBest2014(star_mass=<Quantity 2. solMass>, xray_plasma_temperature=<Quantity 10000000. K>,
                     xray_luminosity=<Quantity 1.e+31 erg / s>, cr_padovani_use_l=False, r_min=<Quantity 0.1 AU>,
                     r_max=<Quantity 200. AU>, zr_max=0.7, radial_bins=100, vertical_bins=100, dust_to_gas=0.01,
                     gas_mass=<Quantity 0.001 solMass>, tapering_radius=<Quantity 100. AU>, tapering_gamma=0.75,
                     midplane_temperature_1au=<Quantity 200. K>, atmosphere_temperature_1au=<Quantity 1000. K>,
                     temperature_slope=0.55, molar_mass=<Quantity 2.33 g / mol>, inner_radius=<Quantity 1. AU>,
                     inner_depletion=1e-06)

    >>> # Generate physics on 3x3 grid
    >>> physics_small_grid = WilliamsBest2014(vertical_bins=3, radial_bins=3)
    >>> # table attribute stores the table with the model. Called first time, calculates the table
    >>> table = physics_small_grid.table
    >>> # print table with floats in exponential format
    >>> table  # doctest: +NORMALIZE_WHITESPACE
    <CTable length=9>
       Radius       Height    Height to radius Gas density  Dust density Gas temperature Dust temperature
         AU           AU                         g / cm3      g / cm3           K               K
      float64      float64        float64        float64      float64        float64         float64
    ------------ ------------ ---------------- ------------ ------------ --------------- ----------------
    1.000000e-01 0.000000e+00     0.000000e+00 1.153290e-15 1.153290e-17    7.096268e+02     7.096268e+02
    1.000000e-01 3.500000e-02     3.500000e-01 5.024587e-34 5.024587e-36    3.548134e+03     3.548134e+03
    1.000000e-01 7.000000e-02     7.000000e-01 5.921268e-72 5.921268e-74    3.548134e+03     3.548134e+03
    7.071068e+00 0.000000e+00     0.000000e+00 2.285168e-13 2.285168e-15    6.820453e+01     6.820453e+01
    7.071068e+00 2.474874e+00     3.500000e-01 2.716386e-17 2.716386e-19    3.410227e+02     3.410227e+02
    7.071068e+00 4.949747e+00     7.000000e-01 7.189026e-23 7.189026e-25    3.410227e+02     3.410227e+02
    5.000000e+02 0.000000e+00     0.000000e+00 2.871710e-20 2.871710e-22    6.555359e+00     6.555359e+00
    5.000000e+02 1.750000e+02     3.500000e-01 6.929036e-22 6.929036e-24    2.625083e+01     2.625083e+01
    5.000000e+02 3.500000e+02     7.000000e-01 8.170464e-23 8.170464e-25    3.277680e+01     3.277680e+01
    >>> physics_wrong_unit = WilliamsBest2014(star_mass=2)  # Does NOT raise an exception
    >>> physics_wrong_unit.table  # but will cause unit conversion errors later
    Traceback (most recent call last):
       ...
    astropy.units.core.UnitConversionError: 'AU(3/2) J(1/2) kg(1/2) s / (g(1/2) m(3/2))' and 'AU' (length) are not convertible
    """
    gas_mass: u.solMass = 1e-3 * u.solMass
    tapering_radius: u.au = 100 * u.au
    tapering_gamma: float = 0.75
    midplane_temperature_1au: u.K = 200 * u.K
    atmosphere_temperature_1au: u.K = 1000 * u.K
    temperature_slope: float = 0.55
    molar_mass: u.kg / u.mol = 2.33 * u.g / u.mol
    inner_radius: u.au = 1 * u.au
    inner_depletion: float = 1e-6

    @u.quantity_input
    def gas_temperature(self, r: u.au, z: u.au) -> u.K:
        """Function that returns gas temperature

        at given r,z using the parametrization from
        Williams & Best 2014, Eq. 5-7
        https://iopscience.iop.org/article/10.1088/0004-637X/788/1/59/pdf

        Args:
            r: u.au -- radial distance
            z: u.au -- height
        Return:
            temperature: u.K
        Raises:
            astropy.unit.UnitConversion

            Error if units are not consistent
        """
        temp_midplane = self.midplane_temperature_1au * (r.to(u.au) / u.au) ** (-self.temperature_slope)
        temp_atmosphere = self.atmosphere_temperature_1au * (r.to(u.au) / u.au) ** (-self.temperature_slope)
        pressure_scalehight = (
                (
                        const.R * temp_midplane * r ** 3 /
                        (const.G * self.star_mass * self.molar_mass)
                ) ** 0.5
        ).to(u.au)
        temperature = u.Quantity(np.zeros_like(z)).value << u.K
        indices_atmosphere = z >= 4 * pressure_scalehight
        indices_midplane = ~ indices_atmosphere
        temperature[indices_atmosphere] = temp_atmosphere[indices_atmosphere]
        temperature[indices_midplane] = (
                temp_midplane[indices_midplane]
                + (temp_atmosphere[indices_midplane] - temp_midplane[indices_midplane])
                * np.sin((np.pi * z[indices_midplane] / (8 * pressure_scalehight[indices_midplane]))
                         .to(u.rad, equivalencies=u.dimensionless_angles())
                         ) ** 4
        )

        return temperature

    @u.quantity_input
    def gas_density(self, r: u.au, z: u.au) -> u.g / u.cm ** 3:
        """
        Calculates gas density according to Eq. 1-3
        Args:
            r: u.au -- radial distance
            z: u.au -- height

        Returns:
            gas density, in u.g / u.cm ** 3
        """
        density = np.zeros_like(z.value) << u.g / u.cm ** 3
        for _r in set(r):
            indices_this_radius = r == _r
            density[indices_this_radius] = self._vertical_density(_r)(z[indices_this_radius])
        return density

    @u.quantity_input
    def _vertical_density(self, _r: u.au, zsteps: int = 100, maxzr: float = 1):
        """
        Calculates unnormalized vertical density according to Eq. 1

        Args:
            _r: u.au -- a single radius to calculate vertical scale

        Returns:
            callable to calculate vertical density
        """
        z = np.linspace(0, _r * maxzr, zsteps).to(u.au)
        r = np.ones_like(z).value * _r
        local_temperature = self.gas_temperature(r, z)
        unscaled_log_density = scipy.integrate.cumtrapz(
            self._vertical_density_integrator(_r, z, local_temperature), z, initial=0
        )
        unscaled_density = np.exp(unscaled_log_density)
        total_density = np.trapz(unscaled_density, z)
        scaled_density = (unscaled_density / total_density * self.column_density(_r)).to(u.g / u.cm ** 3)
        callable = scipy.interpolate.interp1d(z, scaled_density)

        def quantity_callable(z: u.au) -> u.g / u.cm ** 3:
            return callable(z.to(u.au)) << (u.g / u.cm ** 3)

        return quantity_callable

    def _vertical_density_integrator(self, r, z, t):
        dlntdz = np.zeros_like(t).value << (1 / u.au)
        dlntdz[:-1] = (t[1:] - t[:-1]) / (z[1:] - z[:-1]) / (t[:-1])
        return -(
                const.G * self.star_mass * z / (r ** 2 + z ** 2) ** 1.5
                * self.molar_mass / const.R / t
                + dlntdz
        ).to(1 / u.au)

    @u.quantity_input
    def dust_density(self, r: u.au, z: u.au) -> u.g / u.cm ** 3:
        return self.gas_density(r, z) * self.dust_to_gas

    dust_temperature = gas_temperature

    @cached_property
    def column_density_norm(self) -> u.g / u.cm ** 2:
        return (2 - self.tapering_gamma) * self.gas_mass / (2 * np.pi * self.tapering_radius ** 2) \
               * np.exp((self.inner_radius / self.tapering_radius) ** (2 - self.tapering_gamma))

    @u.quantity_input
    def column_density(self, r: u.au) -> u.g / u.cm ** 2:
        """Gas column density at given radius"""
        drop = np.where(r < self.inner_radius, self.inner_depletion, 1)
        return drop * self.column_density_norm * (r / self.tapering_radius) ** (-self.tapering_gamma) \
               * np.exp(-(r / self.tapering_radius) ** (2 - self.tapering_gamma))


@dataclass
class WilliamsBest100au(WilliamsBest2014):
    """
    A subclass of WilliamsBest2014 which uses temperatures at 100 au rather than at 1 au for a more robust fitting

    These defintions are equivalent:
    >>> t_a, t_m, sl = 1000, 200, 0.55
    >>> original = WilliamsBest2014(vertical_bins=3, radial_bins=3,
    ...     midplane_temperature_1au=t_m * u.K, atmosphere_temperature_1au=t_a * u.K,
    ...     temperature_slope=sl)
    >>> at_100_au = WilliamsBest100au(vertical_bins=3, radial_bins=3,
    ...     midplane_temperature_100au=t_m * u.K / 100**sl, atmosphere_temperature_100au = t_a * u.K / 100**sl,
    ...     temperature_slope=sl)
    >>> original.table     # doctest: +NORMALIZE_WHITESPACE
    <CTable length=9>
       Radius       Height    Height to radius Gas density  Dust density Gas temperature Dust temperature
         AU           AU                         g / cm3      g / cm3           K               K
      float64      float64        float64        float64      float64        float64         float64
    ------------ ------------ ---------------- ------------ ------------ --------------- ----------------
    1.000000e-01 0.000000e+00     0.000000e+00 1.153290e-15 1.153290e-17    7.096268e+02     7.096268e+02
    1.000000e-01 3.500000e-02     3.500000e-01 5.024587e-34 5.024587e-36    3.548134e+03     3.548134e+03
    1.000000e-01 7.000000e-02     7.000000e-01 5.921268e-72 5.921268e-74    3.548134e+03     3.548134e+03
    7.071068e+00 0.000000e+00     0.000000e+00 2.285168e-13 2.285168e-15    6.820453e+01     6.820453e+01
    7.071068e+00 2.474874e+00     3.500000e-01 2.716386e-17 2.716386e-19    3.410227e+02     3.410227e+02
    7.071068e+00 4.949747e+00     7.000000e-01 7.189026e-23 7.189026e-25    3.410227e+02     3.410227e+02
    5.000000e+02 0.000000e+00     0.000000e+00 2.871710e-20 2.871710e-22    6.555359e+00     6.555359e+00
    5.000000e+02 1.750000e+02     3.500000e-01 6.929036e-22 6.929036e-24    2.625083e+01     2.625083e+01
    5.000000e+02 3.500000e+02     7.000000e-01 8.170464e-23 8.170464e-25    3.277680e+01     3.277680e+01
    >>> str(at_100_au.table) == str(original.table)
    True
    """
    midplane_temperature_100au: u.K = 15.8866 * u.K
    atmosphere_temperature_100au: u.K = 79.4328 * u.K
    midplane_temperature_1au: u.K = None
    atmosphere_temperature_1au: u.K = None

    def __post_init__(self):
        super().__post_init__()
        if self.midplane_temperature_1au is not None:
            self.logger.warning("'midplane_temperature_1au' is ignored, use 'midplane_temperature_100au' instead")
        if self.atmosphere_temperature_1au is not None:
            self.logger.warning("'atmosphere_temperature_1au' is ignored, use 'atmosphere_temperature_100au' instead")

        self.midplane_temperature_1au = self.midplane_temperature_100au * 100 ** self.temperature_slope
        self.atmosphere_temperature_1au = self.atmosphere_temperature_100au * 100 ** self.temperature_slope


@dataclass
class WB100auWithSmoothInnerGap(WilliamsBest100au):
    @cached_property
    def column_density_norm(self) -> u.g / u.cm ** 2:
        _r = np.geomspace(self.inner_radius / 10, self.tapering_radius * 10, 1000).to(u.au)
        return self.gas_mass / np.trapz(2 * np.pi * _r * self._column_density_unscaled(_r), _r)

    @u.quantity_input
    def _column_density_unscaled(self, r: u.au) -> u.dimensionless_unscaled:
        return (r / self.tapering_radius) ** (-self.tapering_gamma) \
               * np.exp(-(r / self.tapering_radius) ** (2 - self.tapering_gamma)) \
               * np.exp(-(r / self.inner_radius) ** (self.tapering_gamma - 2))

    @u.quantity_input
    def column_density(self, r: u.au) -> u.g / u.cm ** 2:
        """Gas column density at given radius"""
        return self.column_density_norm * self._column_density_unscaled(r)

Classes

class WB100auWithSmoothInnerGap (star_mass: Unit("solMass") = <Quantity 1. solMass>, xray_plasma_temperature: Unit("K") = <Quantity 10000000. K>, xray_luminosity: Unit("erg / s") = <Quantity 1.e+31 erg / s>, cr_padovani_use_l: bool = False, r_min: Unit("AU") = <Quantity 0.1 AU>, r_max: Unit("AU") = <Quantity 500. AU>, zr_max: float = 0.7, radial_bins: int = 100, vertical_bins: int = 100, dust_to_gas: float = 0.01, gas_mass: Unit("solMass") = <Quantity 0.001 solMass>, tapering_radius: Unit("AU") = <Quantity 100. AU>, tapering_gamma: float = 0.75, midplane_temperature_1au: Unit("K") = None, atmosphere_temperature_1au: Unit("K") = None, temperature_slope: float = 0.55, molar_mass: Unit("kg / mol") = <Quantity 2.33 g / mol>, inner_radius: Unit("AU") = <Quantity 1. AU>, inner_depletion: float = 1e-06, midplane_temperature_100au: Unit("K") = <Quantity 15.8866 K>, atmosphere_temperature_100au: Unit("K") = <Quantity 79.4328 K>)

WB100auWithSmoothInnerGap(star_mass: Unit("solMass") = , xray_plasma_temperature: Unit("K") = , xray_luminosity: Unit("erg / s") = , cr_padovani_use_l: bool = False, r_min: Unit("AU") = , r_max: Unit("AU") = , zr_max: float = 0.7, radial_bins: int = 100, vertical_bins: int = 100, dust_to_gas: float = 0.01, gas_mass: Unit("solMass") = , tapering_radius: Unit("AU") = , tapering_gamma: float = 0.75, midplane_temperature_1au: Unit("K") = None, atmosphere_temperature_1au: Unit("K") = None, temperature_slope: float = 0.55, molar_mass: Unit("kg / mol") = , inner_radius: Unit("AU") = , inner_depletion: float = 1e-06, midplane_temperature_100au: Unit("K") = , atmosphere_temperature_100au: Unit("K") = )

Expand source code
@dataclass
class WB100auWithSmoothInnerGap(WilliamsBest100au):
    @cached_property
    def column_density_norm(self) -> u.g / u.cm ** 2:
        _r = np.geomspace(self.inner_radius / 10, self.tapering_radius * 10, 1000).to(u.au)
        return self.gas_mass / np.trapz(2 * np.pi * _r * self._column_density_unscaled(_r), _r)

    @u.quantity_input
    def _column_density_unscaled(self, r: u.au) -> u.dimensionless_unscaled:
        return (r / self.tapering_radius) ** (-self.tapering_gamma) \
               * np.exp(-(r / self.tapering_radius) ** (2 - self.tapering_gamma)) \
               * np.exp(-(r / self.inner_radius) ** (self.tapering_gamma - 2))

    @u.quantity_input
    def column_density(self, r: u.au) -> u.g / u.cm ** 2:
        """Gas column density at given radius"""
        return self.column_density_norm * self._column_density_unscaled(r)

Ancestors

Class variables

var atmosphere_temperature_100au : Unit("K")
var atmosphere_temperature_1au : Unit("K")
var midplane_temperature_100au : Unit("K")
var midplane_temperature_1au : Unit("K")

Instance variables

var column_density_norm
Expand source code
def __get__(self, instance, owner=None):
    if instance is None:
        return self
    if self.attrname is None:
        raise TypeError(
            "Cannot use cached_property instance without calling __set_name__ on it.")
    try:
        cache = instance.__dict__
    except AttributeError:  # not all objects have __dict__ (e.g. class defines slots)
        msg = (
            f"No '__dict__' attribute on {type(instance).__name__!r} "
            f"instance to cache {self.attrname!r} property."
        )
        raise TypeError(msg) from None
    val = cache.get(self.attrname, _NOT_FOUND)
    if val is _NOT_FOUND:
        with self.lock:
            # check if another thread filled cache while we awaited lock
            val = cache.get(self.attrname, _NOT_FOUND)
            if val is _NOT_FOUND:
                val = self.func(instance)
                try:
                    cache[self.attrname] = val
                except TypeError:
                    msg = (
                        f"The '__dict__' attribute on {type(instance).__name__!r} instance "
                        f"does not support item assignment for caching {self.attrname!r} property."
                    )
                    raise TypeError(msg) from None
    return val

Inherited members

class WilliamsBest100au (star_mass: Unit("solMass") = <Quantity 1. solMass>, xray_plasma_temperature: Unit("K") = <Quantity 10000000. K>, xray_luminosity: Unit("erg / s") = <Quantity 1.e+31 erg / s>, cr_padovani_use_l: bool = False, r_min: Unit("AU") = <Quantity 0.1 AU>, r_max: Unit("AU") = <Quantity 500. AU>, zr_max: float = 0.7, radial_bins: int = 100, vertical_bins: int = 100, dust_to_gas: float = 0.01, gas_mass: Unit("solMass") = <Quantity 0.001 solMass>, tapering_radius: Unit("AU") = <Quantity 100. AU>, tapering_gamma: float = 0.75, midplane_temperature_1au: Unit("K") = None, atmosphere_temperature_1au: Unit("K") = None, temperature_slope: float = 0.55, molar_mass: Unit("kg / mol") = <Quantity 2.33 g / mol>, inner_radius: Unit("AU") = <Quantity 1. AU>, inner_depletion: float = 1e-06, midplane_temperature_100au: Unit("K") = <Quantity 15.8866 K>, atmosphere_temperature_100au: Unit("K") = <Quantity 79.4328 K>)

A subclass of WilliamsBest2014 which uses temperatures at 100 au rather than at 1 au for a more robust fitting

These defintions are equivalent:

>>> t_a, t_m, sl = 1000, 200, 0.55
>>> original = WilliamsBest2014(vertical_bins=3, radial_bins=3,
...     midplane_temperature_1au=t_m * u.K, atmosphere_temperature_1au=t_a * u.K,
...     temperature_slope=sl)
>>> at_100_au = WilliamsBest100au(vertical_bins=3, radial_bins=3,
...     midplane_temperature_100au=t_m * u.K / 100**sl, atmosphere_temperature_100au = t_a * u.K / 100**sl,
...     temperature_slope=sl)
>>> original.table     # doctest: +NORMALIZE_WHITESPACE
<CTable length=9>
   Radius       Height    Height to radius Gas density  Dust density Gas temperature Dust temperature
     AU           AU                         g / cm3      g / cm3           K               K
  float64      float64        float64        float64      float64        float64         float64
------------ ------------ ---------------- ------------ ------------ --------------- ----------------
1.000000e-01 0.000000e+00     0.000000e+00 1.153290e-15 1.153290e-17    7.096268e+02     7.096268e+02
1.000000e-01 3.500000e-02     3.500000e-01 5.024587e-34 5.024587e-36    3.548134e+03     3.548134e+03
1.000000e-01 7.000000e-02     7.000000e-01 5.921268e-72 5.921268e-74    3.548134e+03     3.548134e+03
7.071068e+00 0.000000e+00     0.000000e+00 2.285168e-13 2.285168e-15    6.820453e+01     6.820453e+01
7.071068e+00 2.474874e+00     3.500000e-01 2.716386e-17 2.716386e-19    3.410227e+02     3.410227e+02
7.071068e+00 4.949747e+00     7.000000e-01 7.189026e-23 7.189026e-25    3.410227e+02     3.410227e+02
5.000000e+02 0.000000e+00     0.000000e+00 2.871710e-20 2.871710e-22    6.555359e+00     6.555359e+00
5.000000e+02 1.750000e+02     3.500000e-01 6.929036e-22 6.929036e-24    2.625083e+01     2.625083e+01
5.000000e+02 3.500000e+02     7.000000e-01 8.170464e-23 8.170464e-25    3.277680e+01     3.277680e+01
>>> str(at_100_au.table) == str(original.table)
True
Expand source code
@dataclass
class WilliamsBest100au(WilliamsBest2014):
    """
    A subclass of WilliamsBest2014 which uses temperatures at 100 au rather than at 1 au for a more robust fitting

    These defintions are equivalent:
    >>> t_a, t_m, sl = 1000, 200, 0.55
    >>> original = WilliamsBest2014(vertical_bins=3, radial_bins=3,
    ...     midplane_temperature_1au=t_m * u.K, atmosphere_temperature_1au=t_a * u.K,
    ...     temperature_slope=sl)
    >>> at_100_au = WilliamsBest100au(vertical_bins=3, radial_bins=3,
    ...     midplane_temperature_100au=t_m * u.K / 100**sl, atmosphere_temperature_100au = t_a * u.K / 100**sl,
    ...     temperature_slope=sl)
    >>> original.table     # doctest: +NORMALIZE_WHITESPACE
    <CTable length=9>
       Radius       Height    Height to radius Gas density  Dust density Gas temperature Dust temperature
         AU           AU                         g / cm3      g / cm3           K               K
      float64      float64        float64        float64      float64        float64         float64
    ------------ ------------ ---------------- ------------ ------------ --------------- ----------------
    1.000000e-01 0.000000e+00     0.000000e+00 1.153290e-15 1.153290e-17    7.096268e+02     7.096268e+02
    1.000000e-01 3.500000e-02     3.500000e-01 5.024587e-34 5.024587e-36    3.548134e+03     3.548134e+03
    1.000000e-01 7.000000e-02     7.000000e-01 5.921268e-72 5.921268e-74    3.548134e+03     3.548134e+03
    7.071068e+00 0.000000e+00     0.000000e+00 2.285168e-13 2.285168e-15    6.820453e+01     6.820453e+01
    7.071068e+00 2.474874e+00     3.500000e-01 2.716386e-17 2.716386e-19    3.410227e+02     3.410227e+02
    7.071068e+00 4.949747e+00     7.000000e-01 7.189026e-23 7.189026e-25    3.410227e+02     3.410227e+02
    5.000000e+02 0.000000e+00     0.000000e+00 2.871710e-20 2.871710e-22    6.555359e+00     6.555359e+00
    5.000000e+02 1.750000e+02     3.500000e-01 6.929036e-22 6.929036e-24    2.625083e+01     2.625083e+01
    5.000000e+02 3.500000e+02     7.000000e-01 8.170464e-23 8.170464e-25    3.277680e+01     3.277680e+01
    >>> str(at_100_au.table) == str(original.table)
    True
    """
    midplane_temperature_100au: u.K = 15.8866 * u.K
    atmosphere_temperature_100au: u.K = 79.4328 * u.K
    midplane_temperature_1au: u.K = None
    atmosphere_temperature_1au: u.K = None

    def __post_init__(self):
        super().__post_init__()
        if self.midplane_temperature_1au is not None:
            self.logger.warning("'midplane_temperature_1au' is ignored, use 'midplane_temperature_100au' instead")
        if self.atmosphere_temperature_1au is not None:
            self.logger.warning("'atmosphere_temperature_1au' is ignored, use 'atmosphere_temperature_100au' instead")

        self.midplane_temperature_1au = self.midplane_temperature_100au * 100 ** self.temperature_slope
        self.atmosphere_temperature_1au = self.atmosphere_temperature_100au * 100 ** self.temperature_slope

Ancestors

Subclasses

Class variables

var atmosphere_temperature_100au : Unit("K")
var atmosphere_temperature_1au : Unit("K")
var midplane_temperature_100au : Unit("K")
var midplane_temperature_1au : Unit("K")

Inherited members

class WilliamsBest2014 (star_mass: Unit("solMass") = <Quantity 1. solMass>, xray_plasma_temperature: Unit("K") = <Quantity 10000000. K>, xray_luminosity: Unit("erg / s") = <Quantity 1.e+31 erg / s>, cr_padovani_use_l: bool = False, r_min: Unit("AU") = <Quantity 0.1 AU>, r_max: Unit("AU") = <Quantity 500. AU>, zr_max: float = 0.7, radial_bins: int = 100, vertical_bins: int = 100, dust_to_gas: float = 0.01, gas_mass: Unit("solMass") = <Quantity 0.001 solMass>, tapering_radius: Unit("AU") = <Quantity 100. AU>, tapering_gamma: float = 0.75, midplane_temperature_1au: Unit("K") = <Quantity 200. K>, atmosphere_temperature_1au: Unit("K") = <Quantity 1000. K>, temperature_slope: float = 0.55, molar_mass: Unit("kg / mol") = <Quantity 2.33 g / mol>, inner_radius: Unit("AU") = <Quantity 1. AU>, inner_depletion: float = 1e-06)

Class that defines physical model as in Williams & Best 2014 https://iopscience.iop.org/article/10.1088/0004-637X/788/1/59/pdf

Usage:

>>> # Initializing physics class with default parameters
>>> physics = WilliamsBest2014()
>>> physics  # doctest: +NORMALIZE_WHITESPACE
WilliamsBest2014(star_mass=<Quantity 1. solMass>, xray_plasma_temperature=<Quantity 10000000. K>,
                 xray_luminosity=<Quantity 1.e+31 erg / s>, cr_padovani_use_l=False, r_min=<Quantity 0.1 AU>,
                 r_max=<Quantity 500. AU>, zr_max=0.7, radial_bins=100, vertical_bins=100, dust_to_gas=0.01,
                 gas_mass=<Quantity 0.001 solMass>, tapering_radius=<Quantity 100. AU>,
                 tapering_gamma=0.75, midplane_temperature_1au=<Quantity 200. K>,
                  atmosphere_temperature_1au=<Quantity 1000. K>, temperature_slope=0.55,
                  molar_mass=<Quantity 2.33 g / mol>, inner_radius=<Quantity 1. AU>, inner_depletion=1e-06)
>>> # Defaults can be overridden
>>> WilliamsBest2014(star_mass=2 * u.solMass, r_max=200 * u.au)  # doctest: +NORMALIZE_WHITESPACE
WilliamsBest2014(star_mass=<Quantity 2. solMass>, xray_plasma_temperature=<Quantity 10000000. K>,
                 xray_luminosity=<Quantity 1.e+31 erg / s>, cr_padovani_use_l=False, r_min=<Quantity 0.1 AU>,
                 r_max=<Quantity 200. AU>, zr_max=0.7, radial_bins=100, vertical_bins=100, dust_to_gas=0.01,
                 gas_mass=<Quantity 0.001 solMass>, tapering_radius=<Quantity 100. AU>, tapering_gamma=0.75,
                 midplane_temperature_1au=<Quantity 200. K>, atmosphere_temperature_1au=<Quantity 1000. K>,
                 temperature_slope=0.55, molar_mass=<Quantity 2.33 g / mol>, inner_radius=<Quantity 1. AU>,
                 inner_depletion=1e-06)
>>> # Generate physics on 3x3 grid
>>> physics_small_grid = WilliamsBest2014(vertical_bins=3, radial_bins=3)
>>> # table attribute stores the table with the model. Called first time, calculates the table
>>> table = physics_small_grid.table
>>> # print table with floats in exponential format
>>> table  # doctest: +NORMALIZE_WHITESPACE
<CTable length=9>
   Radius       Height    Height to radius Gas density  Dust density Gas temperature Dust temperature
     AU           AU                         g / cm3      g / cm3           K               K
  float64      float64        float64        float64      float64        float64         float64
------------ ------------ ---------------- ------------ ------------ --------------- ----------------
1.000000e-01 0.000000e+00     0.000000e+00 1.153290e-15 1.153290e-17    7.096268e+02     7.096268e+02
1.000000e-01 3.500000e-02     3.500000e-01 5.024587e-34 5.024587e-36    3.548134e+03     3.548134e+03
1.000000e-01 7.000000e-02     7.000000e-01 5.921268e-72 5.921268e-74    3.548134e+03     3.548134e+03
7.071068e+00 0.000000e+00     0.000000e+00 2.285168e-13 2.285168e-15    6.820453e+01     6.820453e+01
7.071068e+00 2.474874e+00     3.500000e-01 2.716386e-17 2.716386e-19    3.410227e+02     3.410227e+02
7.071068e+00 4.949747e+00     7.000000e-01 7.189026e-23 7.189026e-25    3.410227e+02     3.410227e+02
5.000000e+02 0.000000e+00     0.000000e+00 2.871710e-20 2.871710e-22    6.555359e+00     6.555359e+00
5.000000e+02 1.750000e+02     3.500000e-01 6.929036e-22 6.929036e-24    2.625083e+01     2.625083e+01
5.000000e+02 3.500000e+02     7.000000e-01 8.170464e-23 8.170464e-25    3.277680e+01     3.277680e+01
>>> physics_wrong_unit = WilliamsBest2014(star_mass=2)  # Does NOT raise an exception
>>> physics_wrong_unit.table  # but will cause unit conversion errors later
Traceback (most recent call last):
   ...
astropy.units.core.UnitConversionError: 'AU(3/2) J(1/2) kg(1/2) s / (g(1/2) m(3/2))' and 'AU' (length) are not convertible
Expand source code
@dataclass
class WilliamsBest2014(ParametrizedPhysics):
    """
    Class that defines physical model as in Williams & Best 2014
    https://iopscience.iop.org/article/10.1088/0004-637X/788/1/59/pdf

    Usage:

    >>> # Initializing physics class with default parameters
    >>> physics = WilliamsBest2014()
    >>> physics  # doctest: +NORMALIZE_WHITESPACE
    WilliamsBest2014(star_mass=<Quantity 1. solMass>, xray_plasma_temperature=<Quantity 10000000. K>,
                     xray_luminosity=<Quantity 1.e+31 erg / s>, cr_padovani_use_l=False, r_min=<Quantity 0.1 AU>,
                     r_max=<Quantity 500. AU>, zr_max=0.7, radial_bins=100, vertical_bins=100, dust_to_gas=0.01,
                     gas_mass=<Quantity 0.001 solMass>, tapering_radius=<Quantity 100. AU>,
                     tapering_gamma=0.75, midplane_temperature_1au=<Quantity 200. K>,
                      atmosphere_temperature_1au=<Quantity 1000. K>, temperature_slope=0.55,
                      molar_mass=<Quantity 2.33 g / mol>, inner_radius=<Quantity 1. AU>, inner_depletion=1e-06)
    >>> # Defaults can be overridden
    >>> WilliamsBest2014(star_mass=2 * u.solMass, r_max=200 * u.au)  # doctest: +NORMALIZE_WHITESPACE
    WilliamsBest2014(star_mass=<Quantity 2. solMass>, xray_plasma_temperature=<Quantity 10000000. K>,
                     xray_luminosity=<Quantity 1.e+31 erg / s>, cr_padovani_use_l=False, r_min=<Quantity 0.1 AU>,
                     r_max=<Quantity 200. AU>, zr_max=0.7, radial_bins=100, vertical_bins=100, dust_to_gas=0.01,
                     gas_mass=<Quantity 0.001 solMass>, tapering_radius=<Quantity 100. AU>, tapering_gamma=0.75,
                     midplane_temperature_1au=<Quantity 200. K>, atmosphere_temperature_1au=<Quantity 1000. K>,
                     temperature_slope=0.55, molar_mass=<Quantity 2.33 g / mol>, inner_radius=<Quantity 1. AU>,
                     inner_depletion=1e-06)

    >>> # Generate physics on 3x3 grid
    >>> physics_small_grid = WilliamsBest2014(vertical_bins=3, radial_bins=3)
    >>> # table attribute stores the table with the model. Called first time, calculates the table
    >>> table = physics_small_grid.table
    >>> # print table with floats in exponential format
    >>> table  # doctest: +NORMALIZE_WHITESPACE
    <CTable length=9>
       Radius       Height    Height to radius Gas density  Dust density Gas temperature Dust temperature
         AU           AU                         g / cm3      g / cm3           K               K
      float64      float64        float64        float64      float64        float64         float64
    ------------ ------------ ---------------- ------------ ------------ --------------- ----------------
    1.000000e-01 0.000000e+00     0.000000e+00 1.153290e-15 1.153290e-17    7.096268e+02     7.096268e+02
    1.000000e-01 3.500000e-02     3.500000e-01 5.024587e-34 5.024587e-36    3.548134e+03     3.548134e+03
    1.000000e-01 7.000000e-02     7.000000e-01 5.921268e-72 5.921268e-74    3.548134e+03     3.548134e+03
    7.071068e+00 0.000000e+00     0.000000e+00 2.285168e-13 2.285168e-15    6.820453e+01     6.820453e+01
    7.071068e+00 2.474874e+00     3.500000e-01 2.716386e-17 2.716386e-19    3.410227e+02     3.410227e+02
    7.071068e+00 4.949747e+00     7.000000e-01 7.189026e-23 7.189026e-25    3.410227e+02     3.410227e+02
    5.000000e+02 0.000000e+00     0.000000e+00 2.871710e-20 2.871710e-22    6.555359e+00     6.555359e+00
    5.000000e+02 1.750000e+02     3.500000e-01 6.929036e-22 6.929036e-24    2.625083e+01     2.625083e+01
    5.000000e+02 3.500000e+02     7.000000e-01 8.170464e-23 8.170464e-25    3.277680e+01     3.277680e+01
    >>> physics_wrong_unit = WilliamsBest2014(star_mass=2)  # Does NOT raise an exception
    >>> physics_wrong_unit.table  # but will cause unit conversion errors later
    Traceback (most recent call last):
       ...
    astropy.units.core.UnitConversionError: 'AU(3/2) J(1/2) kg(1/2) s / (g(1/2) m(3/2))' and 'AU' (length) are not convertible
    """
    gas_mass: u.solMass = 1e-3 * u.solMass
    tapering_radius: u.au = 100 * u.au
    tapering_gamma: float = 0.75
    midplane_temperature_1au: u.K = 200 * u.K
    atmosphere_temperature_1au: u.K = 1000 * u.K
    temperature_slope: float = 0.55
    molar_mass: u.kg / u.mol = 2.33 * u.g / u.mol
    inner_radius: u.au = 1 * u.au
    inner_depletion: float = 1e-6

    @u.quantity_input
    def gas_temperature(self, r: u.au, z: u.au) -> u.K:
        """Function that returns gas temperature

        at given r,z using the parametrization from
        Williams & Best 2014, Eq. 5-7
        https://iopscience.iop.org/article/10.1088/0004-637X/788/1/59/pdf

        Args:
            r: u.au -- radial distance
            z: u.au -- height
        Return:
            temperature: u.K
        Raises:
            astropy.unit.UnitConversion

            Error if units are not consistent
        """
        temp_midplane = self.midplane_temperature_1au * (r.to(u.au) / u.au) ** (-self.temperature_slope)
        temp_atmosphere = self.atmosphere_temperature_1au * (r.to(u.au) / u.au) ** (-self.temperature_slope)
        pressure_scalehight = (
                (
                        const.R * temp_midplane * r ** 3 /
                        (const.G * self.star_mass * self.molar_mass)
                ) ** 0.5
        ).to(u.au)
        temperature = u.Quantity(np.zeros_like(z)).value << u.K
        indices_atmosphere = z >= 4 * pressure_scalehight
        indices_midplane = ~ indices_atmosphere
        temperature[indices_atmosphere] = temp_atmosphere[indices_atmosphere]
        temperature[indices_midplane] = (
                temp_midplane[indices_midplane]
                + (temp_atmosphere[indices_midplane] - temp_midplane[indices_midplane])
                * np.sin((np.pi * z[indices_midplane] / (8 * pressure_scalehight[indices_midplane]))
                         .to(u.rad, equivalencies=u.dimensionless_angles())
                         ) ** 4
        )

        return temperature

    @u.quantity_input
    def gas_density(self, r: u.au, z: u.au) -> u.g / u.cm ** 3:
        """
        Calculates gas density according to Eq. 1-3
        Args:
            r: u.au -- radial distance
            z: u.au -- height

        Returns:
            gas density, in u.g / u.cm ** 3
        """
        density = np.zeros_like(z.value) << u.g / u.cm ** 3
        for _r in set(r):
            indices_this_radius = r == _r
            density[indices_this_radius] = self._vertical_density(_r)(z[indices_this_radius])
        return density

    @u.quantity_input
    def _vertical_density(self, _r: u.au, zsteps: int = 100, maxzr: float = 1):
        """
        Calculates unnormalized vertical density according to Eq. 1

        Args:
            _r: u.au -- a single radius to calculate vertical scale

        Returns:
            callable to calculate vertical density
        """
        z = np.linspace(0, _r * maxzr, zsteps).to(u.au)
        r = np.ones_like(z).value * _r
        local_temperature = self.gas_temperature(r, z)
        unscaled_log_density = scipy.integrate.cumtrapz(
            self._vertical_density_integrator(_r, z, local_temperature), z, initial=0
        )
        unscaled_density = np.exp(unscaled_log_density)
        total_density = np.trapz(unscaled_density, z)
        scaled_density = (unscaled_density / total_density * self.column_density(_r)).to(u.g / u.cm ** 3)
        callable = scipy.interpolate.interp1d(z, scaled_density)

        def quantity_callable(z: u.au) -> u.g / u.cm ** 3:
            return callable(z.to(u.au)) << (u.g / u.cm ** 3)

        return quantity_callable

    def _vertical_density_integrator(self, r, z, t):
        dlntdz = np.zeros_like(t).value << (1 / u.au)
        dlntdz[:-1] = (t[1:] - t[:-1]) / (z[1:] - z[:-1]) / (t[:-1])
        return -(
                const.G * self.star_mass * z / (r ** 2 + z ** 2) ** 1.5
                * self.molar_mass / const.R / t
                + dlntdz
        ).to(1 / u.au)

    @u.quantity_input
    def dust_density(self, r: u.au, z: u.au) -> u.g / u.cm ** 3:
        return self.gas_density(r, z) * self.dust_to_gas

    dust_temperature = gas_temperature

    @cached_property
    def column_density_norm(self) -> u.g / u.cm ** 2:
        return (2 - self.tapering_gamma) * self.gas_mass / (2 * np.pi * self.tapering_radius ** 2) \
               * np.exp((self.inner_radius / self.tapering_radius) ** (2 - self.tapering_gamma))

    @u.quantity_input
    def column_density(self, r: u.au) -> u.g / u.cm ** 2:
        """Gas column density at given radius"""
        drop = np.where(r < self.inner_radius, self.inner_depletion, 1)
        return drop * self.column_density_norm * (r / self.tapering_radius) ** (-self.tapering_gamma) \
               * np.exp(-(r / self.tapering_radius) ** (2 - self.tapering_gamma))

Ancestors

Subclasses

Class variables

var atmosphere_temperature_1au : Unit("K")
var gas_mass : Unit("solMass")
var inner_depletion : float
var inner_radius : Unit("AU")
var midplane_temperature_1au : Unit("K")
var molar_mass : Unit("kg / mol")
var tapering_gamma : float
var tapering_radius : Unit("AU")
var temperature_slope : float

Instance variables

var column_density_norm
Expand source code
def __get__(self, instance, owner=None):
    if instance is None:
        return self
    if self.attrname is None:
        raise TypeError(
            "Cannot use cached_property instance without calling __set_name__ on it.")
    try:
        cache = instance.__dict__
    except AttributeError:  # not all objects have __dict__ (e.g. class defines slots)
        msg = (
            f"No '__dict__' attribute on {type(instance).__name__!r} "
            f"instance to cache {self.attrname!r} property."
        )
        raise TypeError(msg) from None
    val = cache.get(self.attrname, _NOT_FOUND)
    if val is _NOT_FOUND:
        with self.lock:
            # check if another thread filled cache while we awaited lock
            val = cache.get(self.attrname, _NOT_FOUND)
            if val is _NOT_FOUND:
                val = self.func(instance)
                try:
                    cache[self.attrname] = val
                except TypeError:
                    msg = (
                        f"The '__dict__' attribute on {type(instance).__name__!r} instance "
                        f"does not support item assignment for caching {self.attrname!r} property."
                    )
                    raise TypeError(msg) from None
    return val

Methods

def column_density(self, r: Unit("AU")) ‑> Unit("g / cm2")

Gas column density at given radius

Expand source code
@u.quantity_input
def column_density(self, r: u.au) -> u.g / u.cm ** 2:
    """Gas column density at given radius"""
    drop = np.where(r < self.inner_radius, self.inner_depletion, 1)
    return drop * self.column_density_norm * (r / self.tapering_radius) ** (-self.tapering_gamma) \
           * np.exp(-(r / self.tapering_radius) ** (2 - self.tapering_gamma))
def dust_temperature(self, r: Unit("AU"), z: Unit("AU")) ‑> Unit("K")

Function that returns gas temperature

at given r,z using the parametrization from Williams & Best 2014, Eq. 5-7 https://iopscience.iop.org/article/10.1088/0004-637X/788/1/59/pdf

Args

r
u.au – radial distance
z
u.au – height

Return

temperature: u.K

Raises

astropy.unit.UnitConversion

Error if units are not consistent

Expand source code
@u.quantity_input
def gas_temperature(self, r: u.au, z: u.au) -> u.K:
    """Function that returns gas temperature

    at given r,z using the parametrization from
    Williams & Best 2014, Eq. 5-7
    https://iopscience.iop.org/article/10.1088/0004-637X/788/1/59/pdf

    Args:
        r: u.au -- radial distance
        z: u.au -- height
    Return:
        temperature: u.K
    Raises:
        astropy.unit.UnitConversion

        Error if units are not consistent
    """
    temp_midplane = self.midplane_temperature_1au * (r.to(u.au) / u.au) ** (-self.temperature_slope)
    temp_atmosphere = self.atmosphere_temperature_1au * (r.to(u.au) / u.au) ** (-self.temperature_slope)
    pressure_scalehight = (
            (
                    const.R * temp_midplane * r ** 3 /
                    (const.G * self.star_mass * self.molar_mass)
            ) ** 0.5
    ).to(u.au)
    temperature = u.Quantity(np.zeros_like(z)).value << u.K
    indices_atmosphere = z >= 4 * pressure_scalehight
    indices_midplane = ~ indices_atmosphere
    temperature[indices_atmosphere] = temp_atmosphere[indices_atmosphere]
    temperature[indices_midplane] = (
            temp_midplane[indices_midplane]
            + (temp_atmosphere[indices_midplane] - temp_midplane[indices_midplane])
            * np.sin((np.pi * z[indices_midplane] / (8 * pressure_scalehight[indices_midplane]))
                     .to(u.rad, equivalencies=u.dimensionless_angles())
                     ) ** 4
    )

    return temperature
def gas_density(self, r: Unit("AU"), z: Unit("AU")) ‑> Unit("g / cm3")

Calculates gas density according to Eq. 1-3

Args

r
u.au – radial distance
z
u.au – height

Returns

gas density, in u.g / u.cm ** 3

Expand source code
@u.quantity_input
def gas_density(self, r: u.au, z: u.au) -> u.g / u.cm ** 3:
    """
    Calculates gas density according to Eq. 1-3
    Args:
        r: u.au -- radial distance
        z: u.au -- height

    Returns:
        gas density, in u.g / u.cm ** 3
    """
    density = np.zeros_like(z.value) << u.g / u.cm ** 3
    for _r in set(r):
        indices_this_radius = r == _r
        density[indices_this_radius] = self._vertical_density(_r)(z[indices_this_radius])
    return density
def gas_temperature(self, r: Unit("AU"), z: Unit("AU")) ‑> Unit("K")

Function that returns gas temperature

at given r,z using the parametrization from Williams & Best 2014, Eq. 5-7 https://iopscience.iop.org/article/10.1088/0004-637X/788/1/59/pdf

Args

r
u.au – radial distance
z
u.au – height

Return

temperature: u.K

Raises

astropy.unit.UnitConversion

Error if units are not consistent

Expand source code
@u.quantity_input
def gas_temperature(self, r: u.au, z: u.au) -> u.K:
    """Function that returns gas temperature

    at given r,z using the parametrization from
    Williams & Best 2014, Eq. 5-7
    https://iopscience.iop.org/article/10.1088/0004-637X/788/1/59/pdf

    Args:
        r: u.au -- radial distance
        z: u.au -- height
    Return:
        temperature: u.K
    Raises:
        astropy.unit.UnitConversion

        Error if units are not consistent
    """
    temp_midplane = self.midplane_temperature_1au * (r.to(u.au) / u.au) ** (-self.temperature_slope)
    temp_atmosphere = self.atmosphere_temperature_1au * (r.to(u.au) / u.au) ** (-self.temperature_slope)
    pressure_scalehight = (
            (
                    const.R * temp_midplane * r ** 3 /
                    (const.G * self.star_mass * self.molar_mass)
            ) ** 0.5
    ).to(u.au)
    temperature = u.Quantity(np.zeros_like(z)).value << u.K
    indices_atmosphere = z >= 4 * pressure_scalehight
    indices_midplane = ~ indices_atmosphere
    temperature[indices_atmosphere] = temp_atmosphere[indices_atmosphere]
    temperature[indices_midplane] = (
            temp_midplane[indices_midplane]
            + (temp_atmosphere[indices_midplane] - temp_midplane[indices_midplane])
            * np.sin((np.pi * z[indices_midplane] / (8 * pressure_scalehight[indices_midplane]))
                     .to(u.rad, equivalencies=u.dimensionless_angles())
                     ) ** 4
    )

    return temperature

Inherited members