def test_roundtrip(
    cubefrequencies=[218.44005, 234.68345, 220.07849, 234.69847, 231.28115] * u.GHz,
    degeneracies=[9, 9, 17, 11, 21],
    xaxis=[45.45959683, 60.92357159, 96.61387286, 122.72191958, 165.34856457] * u.K,
    indices=[3503, 1504, 2500, 116, 3322],
):

    # integrated line over 1 km/s (see dnu)
    onekms = 1 * u.km / u.s / constants.c
    kkms = (
        lte_molecule.line_brightness(
            tex=100 * u.K,
            total_column=1e15 * u.cm ** -2,
            partition_function=1185,
            degeneracy=degeneracies,
            frequency=cubefrequencies,
            energy_upper=xaxis.to(u.erg, u.temperature_energy()),
            einstein_A=einsteinAij[indices],
            dnu=onekms * cubefrequencies,
        )
        * u.km
        / u.s
    )
    col, tem, slope, intcpt = fit_tex(
        xaxis, nupper_of_kkms(kkms, cubefrequencies, einsteinAij[indices], degeneracies).value, plot=True
    )
    print("temperature = {0} (input was 100)".format(tem))
    print("column = {0} (input was 1e15)".format(np.log10(col.value)))
Example #2
0
def test_add_equivelencies():
    e1 = u.pixel_scale(10*u.arcsec/u.pixel) + u.temperature_energy()
    assert isinstance(e1, Equivalency)
    assert e1.name == ["pixel_scale", "temperature_energy"]
    assert isinstance(e1.kwargs, list)
    assert e1.kwargs == [dict({'pixscale': 10*u.arcsec/u.pix}), dict()]

    e2 = u.pixel_scale(10*u.arcsec/u.pixel) + [1, 2,3]
    assert isinstance(e2, list)
Example #3
0
def thermal_deBroglie_wavelength(T_e):
    r"""
    Calculate the thermal deBroglie wavelength for electrons.

    Parameters
    ----------
    T_e: ~astropy.units.Quantity
        Electron temperature.

    Returns
    -------
    lambda_dbTh: ~astropy.units.Quantity
        The thermal deBroglie wavelength for electrons in meters.

    Raises
    ------
    TypeError
        If argument is not a `~astropy.units.Quantity`.

    ~astropy.units.UnitConversionError
        If argument is in incorrect units.

    ValueError
        If argument contains invalid values.

    Warns
    -----
    UserWarning
        If units are not provided and SI units are assumed.

    Notes
    -----
    The thermal deBroglie wavelength is approximately the average deBroglie
    wavelength for electrons in an ideal gas and is given by

    .. math::

       \lambda_dbTh = \frac{h}{\sqrt{2 \pi m_e k_B T_e}}

    Example
    -------
    >>> from astropy import units as u
    >>> thermal_deBroglie_wavelength(1 * u.eV)
    <Quantity 6.91936752e-10 m>
    """
    T_e = T_e.to(u.K, equivalencies=u.temperature_energy())
    lambda_dbTh = h / np.sqrt(2 * np.pi * m_e * k_B * T_e)
    return lambda_dbTh.to(u.m)
    def model(tex, col, einsteinAs=einsteinAs, eupper=eupper):
        tex = u.Quantity(tex, u.K)
        col = u.Quantity(col, u.cm**-2)
        eupper = eupper.to(u.erg, u.temperature_energy())
        einsteinAs = u.Quantity(einsteinAs, u.Hz)

        partition_func = specmodel.calculate_partitionfunction(hnco.data['States'],
                                                               temperature=tex.value)
        assert len(partition_func) == 1
        Q_rot = tuple(partition_func.values())[0]
        return lte_molecule.line_brightness(tex, bandwidth, frequencies,
                                            total_column=col,
                                            partition_function=Q_rot,
                                            degeneracy=degeneracies,
                                            energy_upper=eupper,
                                            einstein_A=einsteinAs)
Example #5
0
 def test_unitless_no_vTh(self):
     """
     Tests distribution function without units, and not passing vTh.
     """
     # converting T to SI then stripping units
     T = self.T.to(u.K, equivalencies=u.temperature_energy())
     T = T.si.value
     distFunc = Maxwellian_speed_1D(v=self.v.si.value,
                                    T=T,
                                    particle=self.particle,
                                    units="unitless")
     errStr = (f"Distribution function should be {self.distFuncTrue} "
               f"and not {distFunc}.")
     assert np.isclose(distFunc,
                       self.distFuncTrue,
                       rtol=1e-8,
                       atol=0.0), errStr
Example #6
0
 def test_unitless_vTh(self):
     """
     Tests distribution function without units, and with passing vTh.
     """
     # converting T to SI then stripping units
     T_e = self.T_e.to(u.K, equivalencies=u.temperature_energy())
     T_e = T_e.si.value
     distFunc = kappa_velocity_1D(v=self.v.si.value,
                                  T=T_e,
                                  kappa=self.kappa,
                                  vTh=self.vTh.si.value,
                                  particle=self.particle,
                                  units="unitless")
     errStr = (f"Distribution function should be {self.distFuncTrue} "
               f"and not {distFunc}.")
     assert np.isclose(distFunc,
                       self.distFuncTrue,
                       rtol=1e-8,
                       atol=0.0), errStr
Example #7
0
    def getLEnergy(self,index=None,unit=1./u.cm):

        '''
        Return the energies of the <ny_up+ny_low> excitation levels.

        In case a single energy level is requested via index the energy value is
        extracted from the array.
                        
        @keyword index: The index. In case of default, all are returned. Can be
                        any array-like object that includes indices
                        
                        (default: None)
        @type index: int/array
        @keyword unit: The unit of the returned values. Can be any valid units 
                       str from the astropy units module (energy), or the unit 
                       itself. 'cm-1' and 'cm^-1' are accepted as well.
                        
                       (default: 1./u.cm)
        @type unit: string/unit
        
        @return: The energies ordered by level index.
        @rtype: float/array
        
        '''
        
        #-- Get the energies in cm^-1
        energies = self.get('level','energy',index)
        
        #-- Convert the units. Grab the unit first
        if isinstance(unit,str) and unit.lower() in ['cm-1','cm^-1']: 
            unit = 1./u.cm 
        elif isinstance(unit,str): 
            unit = getattr(u,unit)
        
        #-- In case of temperature, and extra step is needed
        if (isinstance(unit,u.Quantity) and unit.unit.is_equivalent(u.K)) \
                or (isinstance(unit,u.UnitBase) and unit.is_equivalent(u.K)):
            energies = (energies/u.cm).to(u.erg,equivalencies=u.spectral())
            return energies.to(unit,equivalencies=u.temperature_energy()).value
        else: 
            return (energies/u.cm).to(unit,equivalencies=u.spectral()).value
Example #8
0
    def getWavelength(self, species, unit="micron"):

        """
        Return the wavelength grid for a given species. 
        
        @param species: The dust species (from Dust.dat)
        @type species: string
        
        @keyword unit: The unit of the wavelength. Can be given as u.Unit() 
                       object or as a string representation of those objects.
                       Can range from length, to frequency, and energy
        
                       (default: micron)
        @type unit: str/u.Unit()
        
        @return: wavelength (given unit)
        @rtype: array
        
        
        """

        # -- Convert the units. Grab the unit first
        if isinstance(unit, str) and unit.lower() in ["cm-1", "cm^-1"]:
            unit = 1.0 / u.cm
        elif isinstance(unit, str):
            unit = getattr(u, unit)

        self.readKappas(species)
        if not self.waves.has_key(species):
            return np.empty(0)

        wav = self.waves[species] * u.micron
        # -- In case of temperature, and extra step is needed
        if (isinstance(unit, u.Quantity) and unit.unit.is_equivalent(u.K)) or (
            isinstance(unit, u.UnitBase) and unit.is_equivalent(u.K)
        ):
            wav = wav.to(u.erg, equivalencies=u.spectral())
            return wav.to(unit, equivalencies=u.temperature_energy()).value
        else:
            return wav.to(unit, equivalencies=u.spectral()).value
Example #9
0
def test_Debye_number():
    r"""Test the Debye_number function in parameters.py."""

    assert Debye_number(T_e, n_e).unit == u.dimensionless_unscaled

    T_e_eV = T_e.to(u.eV, equivalencies=u.temperature_energy())
    assert np.isclose(Debye_number(T_e, n_e).value,
                      Debye_number(T_e_eV, n_e).value)

    assert np.isclose(Debye_number(
        1 * u.eV, 1 * u.cm**-3).value, 1720862385.43342)

    with pytest.raises(UserWarning):
        Debye_number(T_e, 4)

    with pytest.raises(TypeError):
        Debye_number(None, n_e)

    with pytest.raises(u.UnitConversionError):
        Debye_number(5 * u.m, 5 * u.m**-3)

    with pytest.raises(u.UnitConversionError):
        Debye_number(5 * u.K, 5 * u.m**3)

    with pytest.raises(ValueError):
        Debye_number(5j * u.K, 5 * u.cm**-3)

    Tarr2 = np.array([1, 2]) * u.K
    narr3 = np.array([1, 2, 3]) * u.m**-3
    with pytest.raises(ValueError):
        Debye_number(Tarr2, narr3)

    with pytest.raises(UserWarning):
        assert Debye_number(1.1, 1.1) == Debye_number(1.1 * u.K, 1.1 * u.m**-3)

    with pytest.raises(UserWarning):
        assert Debye_number(1.1 * u.K, 1.1) == Debye_number(1.1, 1.1 * u.m**-3)
Example #10
0
def test_Debye_number():
    r"""Test the Debye_number function in parameters.py."""

    assert Debye_number(T_e, n_e).unit.is_equivalent(u.dimensionless_unscaled)

    T_e_eV = T_e.to(u.eV, equivalencies=u.temperature_energy())
    assert np.isclose(Debye_number(T_e, n_e).value, Debye_number(T_e_eV, n_e).value)

    assert np.isclose(Debye_number(1 * u.eV, 1 * u.cm ** -3).value, 1720862385.43342)

    with pytest.warns(u.UnitsWarning):
        Debye_number(T_e, 4)

    with pytest.raises(ValueError):
        Debye_number(None, n_e)

    with pytest.raises(u.UnitTypeError):
        Debye_number(5 * u.m, 5 * u.m ** -3)

    with pytest.raises(u.UnitTypeError):
        Debye_number(5 * u.K, 5 * u.m ** 3)

    with pytest.raises(ValueError):
        Debye_number(5j * u.K, 5 * u.cm ** -3)

    Tarr2 = np.array([1, 2]) * u.K
    narr3 = np.array([1, 2, 3]) * u.m ** -3
    with pytest.raises(ValueError):
        Debye_number(Tarr2, narr3)

    with pytest.warns(u.UnitsWarning):
        assert Debye_number(1.1, 1.1) == Debye_number(1.1 * u.K, 1.1 * u.m ** -3)

    with pytest.warns(u.UnitsWarning):
        assert Debye_number(1.1 * u.K, 1.1) == Debye_number(1.1, 1.1 * u.m ** -3)

    assert_can_handle_nparray(Debye_number)
def test_roundtrip(cubefrequencies=[218.44005, 234.68345, 220.07849, 234.69847, 231.28115]*u.GHz,
                   degeneracies=[9, 9, 17, 11, 21],
                   xaxis=[45.45959683,  60.92357159,  96.61387286, 122.72191958, 165.34856457]*u.K,
                   indices=[3503, 1504, 2500, 116, 3322],
                  ):

    # integrated line over 1 km/s (see dnu)
    onekms = 1*u.km/u.s / constants.c
    kkms = lte_molecule.line_brightness(tex=100*u.K,
                                        total_column=1e15*u.cm**-2,
                                        partition_function=1185,
                                        degeneracy=degeneracies,
                                        frequency=cubefrequencies,
                                        energy_upper=xaxis.to(u.erg,
                                                              u.temperature_energy()),
                                        einstein_A=einsteinAij[indices],
                                        dnu=onekms*cubefrequencies) * u.km/u.s
    col, tem, slope, intcpt = fit_tex(xaxis, nupper_of_kkms(kkms,
                                                            cubefrequencies,
                                                            einsteinAij[indices],
                                                            degeneracies).value,
                                      plot=True)
    print("temperature = {0} (input was 100)".format(tem))
    print("column = {0} (input was 1e15)".format(np.log10(col.value)))
Example #12
0
def gyroradius(B, *args, Vperp=None, T_i=None, particle='e'):
    r"""Returns the particle gyroradius.

    Parameters
    ----------
    B: Quantity
        The magnetic field magnitude in units convertible to tesla.

    Vperp: Quantity, optional
        The component of particle velocity that is perpendicular to the
        magnetic field in units convertible to meters per second.

    T_i: Quantity, optional
        The particle temperature in units convertible to kelvin.

    particle : string, optional
        Representation of the particle species (e.g., 'p' for protons, 'D+'
        for deuterium, or 'He-4 +1' for singly ionized helium-4),
        which defaults to electrons.  If no charge state information is
        provided, then the particles are assumed to be singly charged.

    args : Quantity
        If the second positional argument is a Quantity with units
        appropriate to Vperp or T_i, then this argument will take the
        place of that keyword argument.

    Returns
    -------
    r_Li : Quantity
        The particle gyroradius in units of meters.  This Quantity will be
        based on either the perpendicular component of particle velocity as
        inputted, or the most probable speed for an particle within a
        Maxwellian distribution for the particle temperature.

    Raises
    ------
    TypeError
        The arguments are of an incorrect type

    UnitConversionError
        The arguments do not have appropriate units

    ValueError
        If any argument contains invalid values

    UserWarning
        If units are not provided and SI units are assumed

    Notes
    -----
    One but not both of Vperp and T_i must be inputted.

    If any of B, Vperp, or T_i is a number rather than a Quantity,
    then SI units will be assumed and a warning will be raised.

    Formula
    -------
    The particle gyroradius is also known as the particle Larmor radius and is
    given by

    .. math::
    r_{Li} = \frac{V_{\perp}}{omega_{ci}}

    where :math:`V_{\perp}` is the component of particle velocity that is
    perpendicular to the magnetic field and :math:`\omega_{ci}` is the
    particle gyrofrequency.  If a temperature is provided, then
    :math:`V_\perp` will be the most probable thermal velocity of an
    particle at that temperature.

    Examples
    --------
    >>> from astropy import units as u
    >>> gyroradius(0.2*u.T, 1e5*u.K, particle='p')
    <Quantity 0.002120874971411475 m>
    >>> gyroradius(0.2*u.T, 1e5*u.K, particle='p')
    <Quantity 0.002120874971411475 m>
    >>> gyroradius(5*u.uG, 1*u.eV, particle='alpha')
    <Quantity 288002.38837768475 m>
    >>> gyroradius(400*u.G, 1e7*u.m/u.s, particle='Fe+++')
    <Quantity 48.23129811339086 m>
    >>> gyroradius(B = 0.01*u.T, T_i = 1e6*u.K)
    <Quantity 0.0031303339253265536 m>
    >>> gyroradius(B = 0.01*u.T, Vperp = 1e6*u.m/u.s)
    <Quantity 0.0005685630062091092 m>
    >>> gyroradius(0.2*u.T, 1e5*u.K)
    <Quantity 4.9494925204636764e-05 m>
    >>> gyroradius(5*u.uG, 1*u.eV)
    <Quantity 6744.259818299466 m>
    >>> gyroradius(400*u.G, 1e7*u.m/u.s)
    <Quantity 0.0014214075155227729 m>

    """

    if Vperp is not None and T_i is not None:
        raise ValueError("Cannot have both Vperp and T_i as arguments to "
                         "gyroradius")

    if len(args) == 1 and isinstance(args[0], units.Quantity):
        arg = args[0].si
        if arg.unit == units.T and B.si.unit in [
                units.J, units.K, units.m / units.s
        ]:
            B, arg = arg, B

        if arg.unit == units.m / units.s:
            Vperp = arg
        elif arg.unit in (units.J, units.K):
            T_i = arg.to(units.K, equivalencies=units.temperature_energy())
        else:
            raise units.UnitConversionError("Incorrect units for positional "
                                            "argument in gyroradius")
    elif len(args) > 0:
        raise ValueError("Incorrect inputs to gyroradius")

    _check_quantity(B, 'B', 'gyroradius', units.T)

    if Vperp is not None:
        _check_quantity(Vperp, 'Vperp', 'gyroradius', units.m / units.s)
    elif T_i is not None:
        _check_quantity(T_i, 'T_i', 'gyroradius', units.K)
        Vperp = thermal_speed(T_i, particle=particle)

    omega_ci = gyrofrequency(B, particle)

    r_Li = np.abs(Vperp) / omega_ci

    return r_Li.to(units.m, equivalencies=units.dimensionless_angles())
Example #13
0
def ion_sound_speed(*ignore,
                    T_e=0 * units.K,
                    T_i=0 * units.K,
                    gamma_e=1,
                    gamma_i=3,
                    ion='p'):
    r"""Returns the ion sound speed for an electron-ion plasma.

    Parameters
    ----------
    T_e : Quantity, optional
        Electron temperature in units of temperature or energy per
        particle.  If this is not given, then the electron temperature
        is assumed to be zero.  If only one temperature is entered, it
        is assumed to be the electron temperature.

    T_i : Quantity, optional
        Ion temperature in units of temperature or energy per
        particle.  If this is not given, then the ion temperature is
        assumed to be zero.

    gamma_e : float or int
        The adiabatic index for electrons, which defaults to 1.  This
        value assumes that the electrons are able to equalize their
        temperature rapidly enough that the electrons are effectively
        isothermal.

    gamma_i : float or int
        The adiabatic index for ions, which defaults to 3.  This value
        assumes that ion motion has only one degree of freedom, namely
        along magnetic field lines.

    ion : string, optional
        Representation of the ion species (e.g., 'p' for protons, 'D+'
        for deuterium, or 'He-4 +1' for singly ionized helium-4),
        which defaults to protons.  If no charge state information is
        provided, then the ions are assumed to be singly charged.

    Returns
    -------
    V_S : Quantity
        The ion sound speed in units of meters per second.

    Raises
    ------
    TypeError
        If any of the arguments are not entered as keyword arguments
        or are of an incorrect type.

    ValueError
        If the ion mass, adiabatic index, or temperature are invalid.

    UnitConversionError
        If the temperature is in incorrect units.

    UserWarning
        If the ion sound speed exceeds 10% of the speed of light, or
        if units are not provided and SI units are assumed.

    Notes
    -----
    The ion sound speed :math:`V_S` is approximately given by

    .. math::
    V_S = \sqrt{\frac{\gamma_e Z k_B T_e + \gamma_i k_B T_i}{m_i}}

    where :math:`\gamma_e` and :math:`\gamma_i` are the electron and
    ion adiabatic indices, :math:`k_B` is the Boltzmann constant,
    :math:`T_e` and :math:`T_i` are the electron and ion temperatures,
    :math:`Z` is the charge state of the ion, and :math:`m_i` is the
    ion mass.

    This function assumes that the product of the wavenumber and the
    Debye length is small. In this limit, the ion sound speed is not
    dispersive (e.g., frequency independent).

    When the electron temperature is much greater than the ion
    temperature, the ion sound velocity reduces to
    :math:`\sqrt{\gamma_e k_B T_e / m_i}`.  Ion acoustic waves can
    therefore occur even when the ion temperature is zero.

    Example
    -------
    >>> from astropy import units as u
    >>> ion_sound_speed(T_e=5e6*u.K, T_i=0*u.K, ion='p', gamma_e=1, gamma_i=3)
    <Quantity 203155.07640420322 m / s>
    >>> ion_sound_speed(T_e=5e6*u.K)
    <Quantity 203155.07640420322 m / s>
    >>> ion_sound_speed(T_e=500*u.eV, T_i=200*u.eV, ion='D+')
    <Quantity 229586.01860212447 m / s>

    """

    if ignore:
        raise TypeError("All arguments are required to be keyword arguments "
                        "in ion_sound_speed to prevent mixing up the electron "
                        "and ion temperatures. An example call that uses the "
                        "units subpackage from astropy is: "
                        "ion_sound_speed(T_e=5*units.K, T_i=0*units.K, "
                        "ion='D+')")

    try:
        m_i = ion_mass(ion)
        Z = charge_state(ion)
        if Z is None:
            Z = 1
    except Exception:
        raise ValueError("Invalid ion in ion_sound_speed.")

    if not isinstance(gamma_e, (float, int)):
        raise TypeError("The adiabatic index for electrons (gamma_e) must be "
                        "a float or int in ion_sound_speed")
    if not isinstance(gamma_i, (float, int)):
        raise TypeError("The adiabatic index for ions (gamma_i) must be "
                        "a float or int in ion_sound_speed")

    if not 1 <= gamma_e <= np.inf:
        raise ValueError("The adiabatic index for electrons must be between "
                         "one and infinity")
    if not 1 <= gamma_i <= np.inf:
        raise ValueError("The adiabatic index for ions must be between "
                         "one and infinity")

    T_i = T_i.to(units.K, equivalencies=units.temperature_energy())
    T_e = T_e.to(units.K, equivalencies=units.temperature_energy())

    try:
        V_S_squared = (gamma_e * Z * k_B * T_e + gamma_i * k_B * T_i) / m_i
        V_S = np.sqrt(V_S_squared).to(units.m / units.s)
    except Exception:
        raise ValueError("Unable to find ion sound speed.")

    return V_S
Example #14
0
class IonizationStateCollection:
    """
    Describe the ionization state distributions of multiple elements
    or isotopes.

    Parameters
    ----------
    inputs: `list`, `tuple`, or `dict`
        A `list` or `tuple` of elements or isotopes (if ``T_e`` is
        provided); a `list` of `~plasmapy.particles.IonizationState`
        instances; a `dict` with elements or isotopes as keys and
        a `~numpy.ndarray` of ionic fractions as the values; or a `dict`
        with elements or isotopes as keys and `~astropy.units.Quantity`
        instances with units of number density.

    abundances: `dict`, optional, keyword-only
        A `dict` with `~plasmapy.particles.particle_class.ParticleLike`
        objects used as the keys and the corresponding relative abundance as the
        values.  The values must be positive real numbers.

    log_abundances: `dict`, optional, keyword-only
        A `dict` with `~plasmapy.particles.particle_class.ParticleLike`
        objects used as the keys and the corresponding base 10 logarithms of their
        relative abundances as the values.  The values must be real numbers.

    n0: `~astropy.units.Quantity`, optional, keyword-only
        The number density normalization factor corresponding to the
        abundances.  The number density of each element is the product
        of its abundance and ``n0``.

    T_e: `~astropy.units.Quantity`, optional, keyword-only
        The electron temperature in units of temperature or thermal
        energy per particle.

    kappa: `float`, optional, keyword-only
        The value of kappa for a kappa distribution function.

    tol: `float` or `integer`, optional, keyword-only
        The absolute tolerance used by `~numpy.isclose` when testing
        normalizations and making comparisons.  Defaults to ``1e-15``.

    Raises
    ------
    `~plasmapy.particles.exceptions.ParticleError`
        If `~plasmapy.particles.IonizationStateCollection` cannot be instantiated.

    See Also
    --------
    ~plasmapy.particles.ionization_state.IonicFraction
    ~plasmapy.particles.ionization_state.IonizationState

    Examples
    --------
    >>> from astropy import units as u
    >>> from plasmapy.particles import IonizationStateCollection
    >>> states = IonizationStateCollection(
    ...     {'H': [0.5, 0.5], 'He': [0.95, 0.05, 0]},
    ...     T_e = 1.2e4 * u.K,
    ...     n0 = 1e15 * u.m ** -3,
    ...     abundances = {'H': 1, 'He': 0.08},
    ... )
    >>> states.ionic_fractions
    {'H': array([0.5, 0.5]), 'He': array([0.95, 0.05, 0.  ])}

    The number densities are given by the ionic fractions multiplied by
    the abundance and the number density scaling factor ``n0``.

    >>> states.number_densities['H']
    <Quantity [5.e+14, 5.e+14] 1 / m3>
    >>> states['He'] = [0.4, 0.59, 0.01]

    To change the ionic fractions for a single element, use item
    assignment.

    >>> states = IonizationStateCollection(['H', 'He'])
    >>> states['H'] = [0.1, 0.9]

    Item assignment will also work if you supply number densities.

    >>> states['He'] = [0.4, 0.6, 0.0] * u.m ** -3
    >>> states.ionic_fractions['He']
    array([0.4, 0.6, 0. ])
    >>> states.number_densities['He']
    <Quantity [0.4, 0.6, 0. ] 1 / m3>

    Notes
    -----
    No more than one of ``abundances`` and ``log_abundances`` may be
    specified.

    If the value provided during item assignment is a
    `~astropy.units.Quantity` with units of number density that retains
    the total element density, then the ionic fractions will be set
    proportionately.

    When making comparisons between `~plasmapy.particles.IonizationStateCollection`
    instances, `~numpy.nan` values are treated as equal.  Equality tests
    are performed to within a tolerance of ``tol``.
    """

    # TODO: Improve explanation of dunder methods in docstring

    # TODO: Add functionality to equilibrate initial ionization states

    @validate_quantities(T_e={"equivalencies": u.temperature_energy()})
    def __init__(
        self,
        inputs: Union[Dict[str, np.ndarray], List, Tuple],
        *,
        T_e: u.K = np.nan * u.K,
        abundances: Optional[Dict[str, Real]] = None,
        log_abundances: Optional[Dict[str, Real]] = None,
        n0: u.m ** -3 = np.nan * u.m ** -3,
        tol: Real = 1e-15,
        kappa: Real = np.inf,
    ):

        abundances_provided = abundances is not None or log_abundances is not None

        set_abundances = True
        if isinstance(inputs, dict):
            all_quantities = np.all(
                [isinstance(fracs, u.Quantity) for fracs in inputs.values()]
            )
            if all_quantities:
                right_units = np.all(
                    [fracs[0].si.unit == u.m ** -3 for fracs in inputs.values()]
                )
                if not right_units:
                    raise ParticleError(
                        "Units must be inverse volume for number densities."
                    )
                if abundances_provided:
                    raise ParticleError(
                        "Abundances cannot be provided if inputs "
                        "provides number density information."
                    )
                set_abundances = False

        try:
            self._pars = dict()
            self.T_e = T_e
            self.n0 = n0
            self.tol = tol
            self.ionic_fractions = inputs
            if set_abundances:
                self.abundances = abundances
                self.log_abundances = log_abundances
            self.kappa = kappa
        except Exception as exc:
            raise ParticleError(
                "Unable to create IonizationStateCollection object."
            ) from exc

    def __str__(self) -> str:
        return f"<IonizationStateCollection for: {', '.join(self.base_particles)}>"

    def __repr__(self) -> str:
        return self.__str__()

    def __getitem__(self, *values) -> Union[IonizationState, IonicFraction]:

        errmsg = f"Invalid indexing for IonizationStateCollection instance: {values[0]}"

        one_input = not isinstance(values[0], tuple)
        two_inputs = len(values[0]) == 2

        if not one_input and not two_inputs:
            raise IndexError(errmsg)

        try:
            arg1 = values[0] if one_input else values[0][0]
            int_charge = None if one_input else values[0][1]
            particle = arg1 if arg1 in self.base_particles else particle_symbol(arg1)

            if int_charge is None:
                return IonizationState(
                    particle=particle,
                    ionic_fractions=self.ionic_fractions[particle],
                    T_e=self._pars["T_e"],
                    n_elem=np.sum(self.number_densities[particle]),
                    tol=self.tol,
                )
            else:
                if not isinstance(int_charge, Integral):
                    raise TypeError(
                        f"{int_charge} is not a valid charge for {particle}."
                    )
                elif not 0 <= int_charge <= atomic_number(particle):
                    raise ChargeError(
                        f"{int_charge} is not a valid charge for {particle}."
                    )
                return IonicFraction(
                    ion=particle_symbol(particle, Z=int_charge),
                    ionic_fraction=self.ionic_fractions[particle][int_charge],
                    number_density=self.number_densities[particle][int_charge],
                )
        except Exception as exc:
            raise IndexError(errmsg) from exc

    def __setitem__(self, key, value):

        errmsg = (
            f"Cannot set item for this IonizationStateCollection instance for "
            f"key = {repr(key)} and value = {repr(value)}"
        )

        try:
            particle = particle_symbol(key)
            self.ionic_fractions[key]
        except (ParticleError, TypeError):
            raise KeyError(
                f"{errmsg} because {repr(key)} is an invalid particle."
            ) from None
        except KeyError:
            raise KeyError(
                f"{errmsg} because {repr(key)} is not one of the base "
                f"particles whose ionization state is being kept track "
                f"of."
            ) from None

        if isinstance(value, u.Quantity) and value.unit != u.dimensionless_unscaled:
            try:
                new_number_densities = value.to(u.m ** -3)
            except u.UnitConversionError:
                raise ValueError(
                    f"{errmsg} because the units of value do not "
                    f"correspond to a number density."
                ) from None

            old_n_elem = np.sum(self.number_densities[particle])
            new_n_elem = np.sum(new_number_densities)

            density_was_nan = np.all(np.isnan(self.number_densities[particle]))
            same_density = u.quantity.allclose(old_n_elem, new_n_elem, rtol=self.tol)

            if not same_density and not density_was_nan:
                raise ValueError(
                    f"{errmsg} because the old element number density "
                    f"of {old_n_elem} is not approximately equal to "
                    f"the new element number density of {new_n_elem}."
                )

            value = (new_number_densities / new_n_elem).to(u.dimensionless_unscaled)

            # If the abundance of this particle has not been defined,
            # then set the abundance if there is enough (but not too
            # much) information to do so.

            abundance_is_undefined = np.isnan(self.abundances[particle])
            isnan_of_abundance_values = np.isnan(list(self.abundances.values()))
            all_abundances_are_nan = np.all(isnan_of_abundance_values)
            n_is_defined = not np.isnan(self.n0)

            if abundance_is_undefined:
                if n_is_defined:
                    self._pars["abundances"][particle] = new_n_elem / self.n0
                elif all_abundances_are_nan:
                    self.n0 = new_n_elem
                    self._pars["abundances"][particle] = 1
                else:
                    raise ParticleError(
                        f"Cannot set number density of {particle} to "
                        f"{value * new_n_elem} when the number density "
                        f"scaling factor is undefined, the abundance "
                        f"of {particle} is undefined, and some of the "
                        f"abundances of other elements/isotopes is "
                        f"defined."
                    )

        try:
            new_fractions = np.array(value, dtype=np.float64)
        except Exception as exc:
            raise TypeError(
                f"{errmsg} because value cannot be converted into an "
                f"array that represents ionic fractions."
            ) from exc

        # TODO: Create a separate function that makes sure ionic
        # TODO: fractions are valid to reduce code repetition.  This
        # TODO: would probably best go as a private function in
        # TODO: ionization_state.py.

        required_nstates = atomic_number(particle) + 1
        new_nstates = len(new_fractions)
        if new_nstates != required_nstates:
            raise ValueError(
                f"{errmsg} because value must have {required_nstates} "
                f"ionization levels but instead corresponds to "
                f"{new_nstates} levels."
            )

        all_nans = np.all(np.isnan(new_fractions))
        if not all_nans and (new_fractions.min() < 0 or new_fractions.max() > 1):
            raise ValueError(
                f"{errmsg} because the new ionic fractions are not "
                f"all between 0 and 1."
            )

        normalized = np.isclose(np.sum(new_fractions), 1, rtol=self.tol)
        if not normalized and not all_nans:
            raise ValueError(
                f"{errmsg} because the ionic fractions are not normalized to one."
            )

        self._ionic_fractions[particle][:] = new_fractions[:]

    def __iter__(self):
        yield from [self[key] for key in self.ionic_fractions.keys()]

    def __eq__(self, other):

        if not isinstance(other, IonizationStateCollection):
            raise TypeError(
                "IonizationStateCollection instance can only be compared with "
                "other IonizationStateCollection instances."
            )

        if self.base_particles != other.base_particles:
            raise ParticleError(
                "Two IonizationStateCollection instances can be compared only "
                "if the base particles are the same."
            )

        min_tol = np.min([self.tol, other.tol])

        # Check any of a whole bunch of equality measures, recalling
        # that np.nan == np.nan is False.

        for attribute in ["T_e", "n_e", "kappa"]:
            this = eval(f"self.{attribute}")
            that = eval(f"other.{attribute}")

            # TODO: Maybe create a function in utils called same_enough
            # TODO: that would take care of all of these disparate
            # TODO: equality measures.

            this_equals_that = np.any(
                [
                    this == that,
                    this is that,
                    np.isnan(this) and np.isnan(that),
                    np.isinf(this) and np.isinf(that),
                    u.quantity.allclose(this, that, rtol=min_tol),
                ]
            )

            if not this_equals_that:
                return False

        for attribute in ["ionic_fractions", "number_densities"]:

            this_dict = eval(f"self.{attribute}")
            that_dict = eval(f"other.{attribute}")

            for particle in self.base_particles:

                this = this_dict[particle]
                that = that_dict[particle]

                this_equals_that = np.any(
                    [
                        this is that,
                        np.all(np.isnan(this)) and np.all(np.isnan(that)),
                        u.quantity.allclose(this, that, rtol=min_tol),
                    ]
                )

                if not this_equals_that:
                    return False

        return True

    @property
    def ionic_fractions(self) -> Dict[str, np.array]:
        """
        Return a `dict` containing the ionic fractions for each element
        and isotope.

        The keys of this `dict` are the symbols for each element or
        isotope.  The values will be `~numpy.ndarray` objects containing
        the ionic fractions for each ionization level corresponding to
        each element or isotope.

        """
        return self._ionic_fractions

    @ionic_fractions.setter
    def ionic_fractions(self, inputs: Union[Dict, List, Tuple]):
        """
        Set the ionic fractions.

        Notes
        -----
        The ionic fractions are initialized during instantiation of
        `~plasmapy.particles.IonizationStateCollection`.  After this, the
        only way to reset the ionic fractions via the ``ionic_fractions``
        attribute is via a `dict` with elements or isotopes that are a
        superset of the previous elements or isotopes.  However, you may
        use item assignment of the `~plasmapy.particles.IonizationState`
        instance to assign new ionic fractions one element or isotope
        at a time.

        Raises
        ------
        `~plasmapy.particles.exceptions.ParticleError`
            If the ionic fractions cannot be set.

        `TypeError`
            If ``inputs`` is not a `list`, `tuple`, or `dict` during
            instantiation, or if ``inputs`` is not a `dict` when it is
            being set.

        """

        # A potential problem is that using item assignment on the
        # ionic_fractions attribute could cause the original attributes
        # to be overwritten without checks being performed.  We might
        # eventually want to create a new class or subclass of UserDict
        # that goes through these checks.  In the meantime, we should
        # make it clear to users to set ionic_fractions by using item
        # assignment on the IonizationStateCollection instance as a whole.  An
        # example of the problem is `s = IonizationStateCollection(["He"])` being
        # followed by `s.ionic_fractions["He"] = 0.3`.

        if hasattr(self, "_ionic_fractions"):
            if not isinstance(inputs, dict):
                raise TypeError(
                    "Can only reset ionic_fractions with a dict if "
                    "ionic_fractions has been set already."
                )
            old_particles = set(self.base_particles)
            new_particles = {particle_symbol(key) for key in inputs.keys()}
            missing_particles = old_particles - new_particles
            if missing_particles:
                raise ParticleError(
                    "Can only reset ionic fractions with a dict if "
                    "the new base particles are a superset of the "
                    "prior base particles.  To change ionic fractions "
                    "for one base particle, use item assignment on the "
                    "IonizationStateCollection instance instead."
                )

        if isinstance(inputs, dict):
            original_keys = inputs.keys()
            ionfrac_types = {type(inputs[key]) for key in original_keys}
            inputs_have_quantities = u.Quantity in ionfrac_types

            if inputs_have_quantities and len(ionfrac_types) != 1:
                raise TypeError(
                    "Ionic fraction information may only be inputted "
                    "as a Quantity object if all ionic fractions are "
                    "Quantity arrays with units of inverse volume."
                )

            try:
                particles = {key: Particle(key) for key in original_keys}
            except (InvalidParticleError, TypeError) as exc:
                raise ParticleError(
                    "Unable to create IonizationStateCollection instance "
                    "because not all particles are valid."
                ) from exc

            # The particles whose ionization states are to be recorded
            # should be elements or isotopes but not ions or neutrals.

            for key in particles.keys():
                is_element = particles[key].is_category("element")
                has_charge_info = particles[key].is_category(
                    any_of=["charged", "uncharged"]
                )

                if not is_element or has_charge_info:
                    raise ParticleError(
                        f"{key} is not an element or isotope without "
                        f"charge information."
                    )

            # We are sorting the elements/isotopes by atomic number and
            # mass number since we will often want to plot and analyze
            # things and this is the most sensible order.

            def _sort_entries_by_atomic_and_mass_numbers(k):
                return (
                    particles[k].atomic_number,
                    particles[k].mass_number if particles[k].isotope else 0,
                )

            sorted_keys = sorted(
                original_keys, key=_sort_entries_by_atomic_and_mass_numbers
            )

            _elements_and_isotopes = []
            _particle_instances = []
            new_ionic_fractions = {}

            if inputs_have_quantities:
                n_elems = {}

            for key in sorted_keys:
                new_key = particles[key].symbol
                _particle_instances.append(particles[key])
                if new_key in _elements_and_isotopes:
                    raise ParticleError(
                        "Repeated particles in IonizationStateCollection."
                    )

                nstates_input = len(inputs[key])
                nstates = particles[key].atomic_number + 1
                if nstates != nstates_input:
                    raise ParticleError(
                        f"The ionic fractions array for {key} must "
                        f"have a length of {nstates}."
                    )

                _elements_and_isotopes.append(new_key)
                if inputs_have_quantities:
                    try:
                        number_densities = inputs[key].to(u.m ** -3)
                        n_elem = np.sum(number_densities)
                        new_ionic_fractions[new_key] = np.array(
                            number_densities / n_elem
                        )
                        n_elems[key] = n_elem
                    except u.UnitConversionError as exc:
                        raise ParticleError("Units are not inverse volume.") from exc
                elif (
                    isinstance(inputs[key], np.ndarray)
                    and inputs[key].dtype.kind == "f"
                ):
                    new_ionic_fractions[particles[key].symbol] = inputs[key]
                else:
                    try:
                        new_ionic_fractions[particles[key].symbol] = np.array(
                            inputs[key], dtype=np.float
                        )
                    except ValueError as exc:
                        raise ParticleError(
                            f"Inappropriate ionic fractions for {key}."
                        ) from exc

            for key in _elements_and_isotopes:
                fractions = new_ionic_fractions[key]
                if not np.all(np.isnan(fractions)):
                    if np.min(fractions) < 0 or np.max(fractions) > 1:
                        raise ParticleError(
                            f"Ionic fractions for {key} are not between 0 and 1."
                        )
                    if not np.isclose(np.sum(fractions), 1, atol=self.tol, rtol=0):
                        raise ParticleError(
                            f"Ionic fractions for {key} are not normalized to 1."
                        )

            # When the inputs provide the densities, the abundances must
            # not have been provided because that would be redundant
            # or contradictory information.  The number density scaling
            # factor might or might not have been provided.  Have the
            # number density scaling factor default to the total number
            # of neutrals and ions across all elements and isotopes, if
            # it was not provided.  Then go ahead and calculate the
            # abundances based on that.  However, we need to be careful
            # that the abundances are not overwritten during the
            # instantiation of the class.

            if inputs_have_quantities:
                if np.isnan(self.n0):
                    new_n = 0 * u.m ** -3
                    for key in _elements_and_isotopes:
                        new_n += n_elems[key]
                    self.n0 = new_n

                new_abundances = {}
                for key in _elements_and_isotopes:
                    new_abundances[key] = np.float(n_elems[key] / self.n0)

                self._pars["abundances"] = new_abundances

        elif isinstance(inputs, (list, tuple)):

            try:
                _particle_instances = [Particle(particle) for particle in inputs]
            except (InvalidParticleError, TypeError) as exc:
                raise ParticleError(
                    "Invalid inputs to IonizationStateCollection."
                ) from exc

            _particle_instances.sort(key=_atomic_number_and_mass_number)

            _elements_and_isotopes = [
                particle.symbol for particle in _particle_instances
            ]
            new_ionic_fractions = {
                particle.symbol: np.full(
                    particle.atomic_number + 1, fill_value=np.nan, dtype=np.float64
                )
                for particle in _particle_instances
            }
        else:
            raise TypeError("Incorrect inputs to set ionic_fractions.")

        for i in range(1, len(_particle_instances)):
            if _particle_instances[i - 1].element == _particle_instances[i].element:
                if (
                    not _particle_instances[i - 1].isotope
                    and _particle_instances[i].isotope
                ):
                    raise ParticleError(
                        "Cannot have an element and isotopes of that element."
                    )

        self._particle_instances = _particle_instances
        self._base_particles = _elements_and_isotopes
        self._ionic_fractions = new_ionic_fractions

    def normalize(self) -> None:
        """
        Normalize the ionic fractions so that the sum for each element
        equals one.
        """
        for particle in self.base_particles:
            tot = np.sum(self.ionic_fractions[particle])
            self.ionic_fractions[particle] = self.ionic_fractions[particle] / tot

    @property
    @validate_quantities
    def n_e(self) -> u.m ** -3:
        """
        Return the electron number density under the assumption of
        quasineutrality.
        """
        number_densities = self.number_densities
        n_e = 0.0 * u.m ** -3
        for elem in self.base_particles:
            atomic_numb = atomic_number(elem)
            number_of_ionization_states = atomic_numb + 1
            integer_charges = np.linspace(0, atomic_numb, number_of_ionization_states)
            n_e += np.sum(number_densities[elem] * integer_charges)
        return n_e

    @property
    @validate_quantities
    def n0(self) -> u.m ** -3:
        """Return the number density scaling factor."""
        return self._pars["n"]

    @n0.setter
    @validate_quantities
    def n0(self, n: u.m ** -3):
        """Set the number density scaling factor."""
        try:
            n = n.to(u.m ** -3)
        except u.UnitConversionError as exc:
            raise ParticleError("Units cannot be converted to u.m ** -3.") from exc
        except Exception as exc:
            raise ParticleError(f"{n} is not a valid number density.") from exc
        if n < 0 * u.m ** -3:
            raise ParticleError("Number density cannot be negative.")
        self._pars["n"] = n.to(u.m ** -3)

    @property
    def number_densities(self) -> Dict[str, u.Quantity]:
        """
        Return a `dict` containing the number densities for element or
        isotope.
        """
        return {
            elem: self.n0 * self.abundances[elem] * self.ionic_fractions[elem]
            for elem in self.base_particles
        }

    @property
    def abundances(self) -> Optional[Dict[ParticleLike, Real]]:
        """Return the elemental abundances."""
        return self._pars["abundances"]

    @abundances.setter
    def abundances(self, abundances_dict: Optional[Dict[ParticleLike, Real]]):
        """
        Set the elemental (or isotopic) abundances.  The elements and
        isotopes must be the same as or a superset of the elements whose
        ionization states are being tracked.
        """
        if abundances_dict is None:
            self._pars["abundances"] = {elem: np.nan for elem in self.base_particles}
        elif not isinstance(abundances_dict, dict):
            raise TypeError(
                f"The abundances attribute must be a dict with "
                f"elements or isotopes as keys and real numbers "
                f"representing relative abundances as values."
            )
        else:
            old_keys = abundances_dict.keys()
            try:
                new_keys_dict = {}
                for old_key in old_keys:
                    new_keys_dict[particle_symbol(old_key)] = old_key
            except Exception:
                raise ParticleError(
                    f"The key {repr(old_key)} in the abundances "
                    f"dictionary is not a valid element or isotope."
                )

            new_elements = new_keys_dict.keys()

            old_elements_set = set(self.base_particles)
            new_elements_set = set(new_elements)

            if old_elements_set - new_elements_set:
                raise ParticleError(
                    f"The abundances of the following particles are "
                    f"missing: {old_elements_set - new_elements_set}"
                )

            new_abundances_dict = {}

            for element in new_elements:
                inputted_abundance = abundances_dict[new_keys_dict[element]]
                try:
                    inputted_abundance = float(inputted_abundance)
                except Exception:
                    raise TypeError(
                        f"The abundance for {element} was provided as"
                        f"{inputted_abundance}, which cannot be "
                        f"converted to a real number."
                    ) from None

                if inputted_abundance < 0:
                    raise ParticleError(f"The abundance of {element} is negative.")
                new_abundances_dict[element] = inputted_abundance

            self._pars["abundances"] = new_abundances_dict

    @property
    def log_abundances(self) -> Dict[str, Real]:
        """
        Return a `dict` with atomic or isotope symbols as keys and the
        base 10 logarithms of the relative abundances as the
        corresponding values.
        """
        log_abundances_dict = {}
        for key in self.abundances.keys():
            log_abundances_dict[key] = np.log10(self.abundances[key])
        return log_abundances_dict

    @log_abundances.setter
    def log_abundances(self, value: Optional[Dict[str, Real]]):
        """
        Set the base 10 logarithm of the relative abundances.
        """
        if value is not None:
            try:
                new_abundances_input = {}
                for key in value.keys():
                    new_abundances_input[key] = 10 ** value[key]
                self.abundances = new_abundances_input
            except Exception:
                raise ParticleError("Invalid log_abundances.") from None

    @property
    @validate_quantities(equivalencies=u.temperature_energy())
    def T_e(self) -> u.K:
        """Return the electron temperature."""
        return self._pars["T_e"]

    @T_e.setter
    @validate_quantities(equivalencies=u.temperature_energy())
    def T_e(self, electron_temperature: u.K):
        """Set the electron temperature."""
        try:
            temperature = electron_temperature.to(
                u.K, equivalencies=u.temperature_energy()
            )
        except (AttributeError, u.UnitsError):
            raise ParticleError(
                f"{electron_temperature} is not a valid temperature."
            ) from None
        if temperature < 0 * u.K:
            raise ParticleError("The electron temperature cannot be negative.")
        self._pars["T_e"] = temperature

    @property
    def kappa(self) -> np.real:
        """
        Return the kappa parameter for a kappa distribution function
        for electrons.

        The value of ``kappa`` must be greater than ``1.5`` in order to
        have a valid distribution function.  If ``kappa`` equals
        `~numpy.inf`, then the distribution function reduces to a
        Maxwellian.

        """
        return self._pars["kappa"]

    @kappa.setter
    def kappa(self, value: Real):
        """
        Set the kappa parameter for a kappa distribution function for
        electrons.  The value must be between ``1.5`` and `~numpy.inf`.
        """
        kappa_errmsg = "kappa must be a real number greater than 1.5"
        if not isinstance(value, Real):
            raise TypeError(kappa_errmsg)
        if value <= 1.5:
            raise ValueError(kappa_errmsg)
        self._pars["kappa"] = np.real(value)

    @property
    def base_particles(self) -> List[str]:
        """
        Return a list of the elements and isotopes whose ionization
        states are being kept track of.
        """
        return self._base_particles

    @property
    def tol(self) -> np.real:
        """Return the absolute tolerance for comparisons."""
        return self._tol

    @tol.setter
    def tol(self, atol: Real):
        """
        Set the absolute tolerance for comparisons.
        """
        if not isinstance(atol, Real):
            raise TypeError("The attribute tol must be a real number.")
        if 0 <= atol <= 1.0:
            self._tol = np.real(atol)
        else:
            raise ValueError("Need 0 <= tol <= 1.")

    def summarize(self, minimum_ionic_fraction: Real = 0.01) -> None:
        """
        Print quicklook information for an
        `~plasmapy.particles.IonizationStateCollection` instance.

        Parameters
        ----------
        minimum_ionic_fraction: `Real`
            If the ionic fraction for a particular ionization state is
            below this level, then information for it will not be
            printed.  Defaults to 0.01.

        Examples
        --------
        >>> states = IonizationStateCollection(
        ...     {'H': [0.1, 0.9], 'He': [0.95, 0.05, 0.0]},
        ...     T_e = 12000 * u.K,
        ...     n0 = 3e9 * u.cm ** -3,
        ...     abundances = {'H': 1.0, 'He': 0.1},
        ...     kappa = 3.4,
        ... )
        >>> states.summarize()
        IonizationStateCollection instance for: H, He
        ----------------------------------------------------------------
        H  0+: 0.100    n_i = 3.00e+14 m**-3
        H  1+: 0.900    n_i = 2.70e+15 m**-3
        ----------------------------------------------------------------
        He  0+: 0.950    n_i = 2.85e+14 m**-3
        He  1+: 0.050    n_i = 1.50e+13 m**-3
        ----------------------------------------------------------------
        n_e = 2.71e+15 m**-3
        T_e = 1.20e+04 K
        kappa = 3.40
        ----------------------------------------------------------------

        """
        separator_line = 64 * "-"

        output = []

        output.append(
            f"IonizationStateCollection instance for: {', '.join(self.base_particles)}"
        )

        # Get the ionic symbol with the corresponding ionic fraction and
        # number density (if available), but only for the most abundant
        # ionization levels for each element.

        for ionization_state in self:
            states_info = ionization_state._get_states_info(minimum_ionic_fraction)
            if len(states_info) > 0:
                output += states_info
                output[-1] += "\n" + separator_line

        attributes = []
        if np.isfinite(self.n_e):
            attributes.append("n_e = " + "{:.2e}".format(self.n_e.value) + " m**-3")
        if np.isfinite(self.T_e):
            attributes.append("T_e = " + "{:.2e}".format(self.T_e.value) + " K")
        if np.isfinite(self.kappa):
            attributes.append("kappa = " + "{:.2f}".format(self.kappa))

        if attributes:
            attributes.append(separator_line)

        output.append("\n".join(attributes))

        if len(output) > 1:
            output[0] += "\n" + separator_line
            output_string = "\n".join(output)
        else:
            output_string = output[0]

        print(output_string.strip("\n"))
Example #15
0
def Maxwellian_1D(v, T, particle="e", V_drift=0, vTh=np.nan, units="units"):
    r"""
    Returns the probability at the velocity `v` in m/s
    to find a particle `particle` in a plasma of temperature `T`
    following the Maxwellian distribution function.

    Parameters
    ----------
    v: ~astropy.units.Quantity
        The velocity in units convertible to m/s.

    T: ~astropy.units.Quantity
        The temperature in Kelvin.

    particle: str, optional
        Representation of the particle species(e.g., `'p'` for protons, `'D+'`
        for deuterium, or `'He-4 +1'` for :math:`He_4^{+1}`
        (singly ionized helium-4),
        which defaults to electrons.

    V_drift: ~astropy.units.Quantity, optional
        The drift velocity in units convertible to m/s.

    vTh: ~astropy.units.Quantity, optional
        Thermal velocity (most probable) in m/s. This is used for
        optimization purposes to avoid re-calculating vTh, for example
        when integrating over velocity-space.

    units: str, optional
        Selects whether to run function with units and unit checks (when
        equal to "units") or to run as unitless (when equal to "unitless").
        The unitless version is substantially faster for intensive
        computations.

    Returns
    -------
    f : ~astropy.units.Quantity
        Probability in Velocity^-1, normalized so that
        :math:`\int_{-\infty}^{+\infty} f(v) dv = 1`.

    Raises
    ------
    TypeError
        The parameter arguments are not Quantities and
        cannot be converted into Quantities.

    ~astropy.units.UnitConversionError
        If the parameters are not in appropriate units.

    ValueError
        If the temperature is negative, or the particle mass or charge state
        cannot be found.

    Notes
    -----
    In one dimension, the Maxwellian distribution function for a particle of
    mass m, velocity v, a drift velocity V and with temperature T is:

    .. math::

        f = \sqrt{\frac{m}{2 \pi k_B T}} e^{-\frac{m}{2 k_B T} (v-V)^2}
        f = (\pi * v_Th^2)^{-1/2} e^{-(v - v_{drift})^2 / v_Th^2}

    where :math:`v_Th = \sqrt(2 k_B T / m)` is the thermal speed

    Examples
    --------
    >>> from plasmapy.physics import Maxwellian_1D
    >>> from astropy import units as u
    >>> v=1*u.m/u.s
    >>> Maxwellian_1D(v=v, T= 30000*u.K, particle='e',V_drift=0*u.m/u.s)
    <Quantity 5.91632969e-07 s / m>
    """
    if units == "units":
        # unit checks and conversions
        # checking velocity units
        v = v.to(u.m / u.s)
        # catching case where drift velocities have default values, they
        # need to be assigned units
        if V_drift == 0:
            if not isinstance(V_drift, astropy.units.quantity.Quantity):
                V_drift = V_drift * u.m / u.s
        # checking units of drift velocities
        V_drift = V_drift.to(u.m / u.s)
        # convert temperature to Kelvins
        T = T.to(u.K, equivalencies=u.temperature_energy())
        if np.isnan(vTh):
            # get thermal velocity and thermal velocity squared
            vTh = (thermal_speed(T, particle=particle, method="most_probable"))
        elif not np.isnan(vTh):
            # check units of thermal velocity
            vTh = vTh.to(u.m / u.s)
    elif np.isnan(vTh) and units == "unitless":
        # assuming unitless temperature is in Kelvins
        vTh = (thermal_speed(T * u.K,
                             particle=particle,
                             method="most_probable")).si.value
    # Get thermal velocity squared
    vThSq = vTh**2
    # Get square of relative particle velocity
    vSq = (v - V_drift)**2
    # calculating distribution function
    coeff = (vThSq * np.pi)**(-1 / 2)
    expTerm = np.exp(-vSq / vThSq)
    distFunc = coeff * expTerm
    if units == "units":
        return distFunc.to(u.s / u.m)
    elif units == "unitless":
        return distFunc
Example #16
0
def kappa_velocity_1D(v,
                      T,
                      kappa,
                      particle="e",
                      V_drift=0,
                      vTh=np.nan,
                      units="units"):
    r"""
    Return the probability at the velocity `v` in m/s
    to find a particle `particle` in a plasma of temperature `T`
    following the Kappa distribution function. The slope of the
    tail of the Kappa distribution function is set by 'kappa', which
    must be greater than :math:`1/2`.

    Parameters
    ----------
    v: ~astropy.units.Quantity
        The velocity in units convertible to m/s.

    T: ~astropy.units.Quantity
        The temperature in Kelvin.

    kappa: ~astropy.units.Quantity
        The kappa parameter is a dimensionless number which sets the slope
        of the energy spectrum of suprathermal particles forming the tail
        of the Kappa velocity distribution function. Kappa must be greater
        than :math:`3/2`.

    particle: str, optional
        Representation of the particle species(e.g., `'p` for protons, `'D+'`
        for deuterium, or `'He-4 +1'` for :math:`He_4^{+1}`
        (singly ionized helium-4), which defaults to electrons.

    V_drift: ~astropy.units.Quantity, optional
        The drift velocity in units convertible to m/s.

    vTh: ~astropy.units.Quantity, optional
        Thermal velocity (most probable) in m/s. This is used for
        optimization purposes to avoid re-calculating `vTh`, for example
        when integrating over velocity-space.

    units: str, optional
        Selects whether to run function with units and unit checks (when
        equal to "units") or to run as unitless (when equal to "unitless").
        The unitless version is substantially faster for intensive
        computations.

    Returns
    -------
    f : ~astropy.units.Quantity
        probability in Velocity^-1, normalized so that
        :math:`\int_{-\infty}^{+\infty} f(v) dv = 1`.

    Raises
    ------
    TypeError
        A parameter argument is not a `~astropy.units.Quantity` and
        cannot be converted into a `~astropy.units.Quantity`.

    ~astropy.units.UnitConversionError
        If the parameters is not in appropriate units.

    ValueError
        If the temperature is negative, or the particle mass or charge state
        cannot be found.

    Notes
    -----
    In one dimension, the Kappa velocity distribution function describing
    the distribution of particles with speed :math:`v` in a plasma with
    temperature :math:`T` and suprathermal parameter :math:`\kappa` is
    given by:

    .. math::

       f = A_\kappa \left(1 + \frac{(\vec{v} -
       \vec{V_{drift}})^2}{\kappa v_Th,\kappa^2}\right)^{-\kappa}

    where :math:`v_Th,\kappa` is the kappa thermal speed
    and :math:`A_\kappa = \frac{1}{\sqrt{\pi} \kappa^{3/2} v_Th,\kappa^2
    \frac{\Gamma(\kappa + 1)}{\Gamma(\kappa - 1/2)}}`
    is the normalization constant.

    As :math:`\kappa` approaches infinity, the kappa distribution function
    converges to the Maxwellian distribution function.

    Examples
    --------
    >>> from plasmapy.physics import kappa_velocity_1D
    >>> from astropy import units as u
    >>> v=1*u.m/u.s
    >>> kappa_velocity_1D(v=v, T=30000*u.K, kappa=4, particle='e',V_drift=0*u.m/u.s)
    <Quantity 6.75549854e-07 s / m>
    """
    # must have kappa > 3/2 for distribution function to be valid
    if kappa <= 3 / 2:
        raise ValueError(f"Must have kappa > 3/2, instead of {kappa}.")
    if units == "units":
        # unit checks and conversions
        # checking velocity units
        v = v.to(u.m / u.s)
        # catching case where drift velocities have default values, they
        # need to be assigned units
        if V_drift == 0:
            if not isinstance(V_drift, astropy.units.quantity.Quantity):
                V_drift = V_drift * u.m / u.s
        # checking units of drift velocities
        V_drift = V_drift.to(u.m / u.s)
        # convert temperature to Kelvins
        T = T.to(u.K, equivalencies=u.temperature_energy())
        if np.isnan(vTh):
            # get thermal velocity and thermal velocity squared
            vTh = kappa_thermal_speed(T, kappa, particle=particle)
        elif not np.isnan(vTh):
            # check units of thermal velocity
            vTh = vTh.to(u.m / u.s)
    elif np.isnan(vTh) and units == "unitless":
        # assuming unitless temperature is in Kelvins
        vTh = (kappa_thermal_speed(T * u.K, kappa, particle=particle)).si.value
    # Get thermal velocity squared and accounting for 1D instead of 3D
    vThSq = vTh**2
    # Get square of relative particle velocity
    vSq = (v - V_drift)**2
    # calculating distribution function
    expTerm = (1 + vSq / (kappa * vThSq))**(-kappa)
    coeff1 = 1 / (np.sqrt(np.pi) * kappa**(3 / 2) * vTh)
    coeff2 = gamma(kappa + 1) / (gamma(kappa - 1 / 2))
    distFunc = coeff1 * coeff2 * expTerm
    if units == "units":
        return distFunc.to(u.s / u.m)
    elif units == "unitless":
        return distFunc
Example #17
0
def Maxwellian_speed_1D(v,
                        T,
                        particle="e",
                        V_drift=0,
                        vTh=np.nan,
                        units="units"):
    r"""
    Return the probability of finding a particle with speed `v` in m/s
    in an equilibrium plasma of temperature `T` which follows the
    Maxwellian distribution function.

    Parameters
    ----------
    v: ~astropy.units.Quantity
        The speed in units convertible to m/s.

    T: ~astropy.units.Quantity
        The temperature, preferably in Kelvin.

    particle: str, optional
        Representation of the particle species(e.g., `'p'` for protons, `'D+'`
        for deuterium, or `'He-4 +1'` for :math:`He_4^{+1}`
        (singly ionized helium-4), which defaults to electrons.

    V_drift: ~astropy.units.Quantity
        The drift speed in units convertible to m/s.

    vTh: ~astropy.units.Quantity, optional
        Thermal velocity (most probable) in m/s. This is used for
        optimization purposes to avoid re-calculating vTh, for example
        when integrating over velocity-space.

    units: str, optional
        Selects whether to run function with units and unit checks (when
        equal to "units") or to run as unitless (when equal to "unitless").
        The unitless version is substantially faster for intensive
        computations.

    Returns
    -------
    f : ~astropy.units.Quantity
        Probability in speed^-1, normalized so that
        :math:`\int_{0}^{\infty} f(v) dv = 1`.

    Raises
    ------
    TypeError
        The parameter arguments are not Quantities and
        cannot be converted into Quantities.

    ~astropy.units.UnitConversionError
        If the parameters is not in appropriate units.

    ValueError
        If the temperature is negative, or the particle mass or charge state
        cannot be found.

    Notes
    -----
    In one dimension, the Maxwellian speed distribution function describing
    the distribution of particles with speed v in a plasma with temperature T
    is given by:

    .. math::

       f(v) = 4 \pi v^2 (\pi * v_Th^2)^{-3/2} \exp(-(v - V_{drift})^2 / v_Th^2)

    where :math:`v_Th = \sqrt(2 k_B T / m)` is the thermal speed.

    Example
    -------
    >>> from plasmapy.physics import Maxwellian_speed_1D
    >>> from astropy import units as u
    >>> v=1*u.m/u.s
    >>> Maxwellian_speed_1D(v=v, T= 30000*u.K, particle='e',V_drift=0*u.m/u.s)
    <Quantity 2.60235754e-18 s / m>

    """
    if units == "units":
        # unit checks and conversions
        # checking velocity units
        v = v.to(u.m / u.s)
        # catching case where drift velocity has default value, and
        # needs to be assigned units
        if V_drift == 0:
            if not isinstance(V_drift, astropy.units.quantity.Quantity):
                V_drift = V_drift * u.m / u.s
        # checking drift velocity units
        V_drift = V_drift.to(u.m / u.s)
        # convert temperature to Kelvins
        T = T.to(u.K, equivalencies=u.temperature_energy())
        if np.isnan(vTh):
            # get thermal velocity and thermal velocity squared
            vTh = (thermal_speed(T, particle=particle, method="most_probable"))
        elif not np.isnan(vTh):
            # check units of thermal velocity
            vTh = vTh.to(u.m / u.s)
    elif np.isnan(vTh) and units == "unitless":
        # assuming unitless temperature is in Kelvins
        vTh = (thermal_speed(T * u.K,
                             particle=particle,
                             method="most_probable")).si.value
    # getting square of thermal speed
    vThSq = vTh**2
    # get square of relative particle speed
    vSq = (v - V_drift)**2
    # calculating distribution function
    coeff1 = (np.pi * vThSq)**(-3 / 2)
    coeff2 = 4 * np.pi * vSq
    expTerm = np.exp(-vSq / vThSq)
    distFunc = coeff1 * coeff2 * expTerm
    if units == "units":
        return distFunc.to(u.s / u.m)
    elif units == "unitless":
        return distFunc
Example #18
0
def collision_rate_electron_ion(T_e,
                                n_e,
                                ion_particle,
                                coulomb_log=None,
                                V=None,
                                coulomb_log_method="classical"):
    r"""
    Momentum relaxation electron-ion collision rate

    From [3]_, equations (2.17) and (2.120)

    Considering a Maxwellian distribution of "test" electrons colliding with
    a Maxwellian distribution of "field" ions.

    This result is an electron momentum relaxation rate, and is used in many
    classical transport expressions. It is equivalent to:
    * 1/tau_e from ref [1]_ eqn (1) pp. #,
    * 1/tau_e from ref [2]_ eqn (1) pp. #,
    * nu_e\i_S from ref [2]_ eqn (1) pp. #,

    Parameters
    ----------
    T_e : ~astropy.units.Quantity
        The electron temperature of the Maxwellian test electrons

    n_e : ~astropy.units.Quantity
        The number density of the Maxwellian test electrons

    ion_particle: str
        String signifying a particle type of the field ions, including charge
        state information.

    V : ~astropy.units.Quantity, optional
        The relative velocity between particles.  If not provided,
        thermal velocity is assumed: :math:`\mu V^2 \sim 2 k_B T`
        where `mu` is the reduced mass.

    coulomb_log : float or dimensionless ~astropy.units.Quantity, optional
        Option to specify a Coulomb logarithm of the electrons on the ions.
        If not specified, the Coulomb log will is calculated using the
        `~plasmapy.physics.transport.Coulomb_logarithm` function.

    coulomb_log_method : string, optional
        Method used for Coulomb logarithm calculation (see that function
        for more documentation). Choose from "classical" or "GMS-1" to "GMS-6".

    References
    ----------
    .. [1] Braginskii

    .. [2] Formulary

    .. [3] Callen Chapter 2, http://homepages.cae.wisc.edu/~callen/chap2.pdf

    Examples
    --------
    >>> from astropy import units as u
    >>> collision_rate_electron_ion(0.1*u.eV, 1e6/u.m**3, 'p')
    <Quantity 0.00180172 1 / s>
    >>> collision_rate_electron_ion(100*u.eV, 1e6/u.m**3, 'p')
    <Quantity 8.6204672e-08 1 / s>
    >>> collision_rate_electron_ion(100*u.eV, 1e20/u.m**3, 'p')
    <Quantity 3936037.8595928 1 / s>
    >>> collision_rate_electron_ion(100*u.eV, 1e20/u.m**3, 'p', coulomb_log_method = 'GMS-1')
    <Quantity 3872922.52743562 1 / s>
    >>> collision_rate_electron_ion(0.1*u.eV, 1e6/u.m**3, 'p', V = c/100)
    <Quantity 4.41166015e-07 1 / s>
    >>> collision_rate_electron_ion(100*u.eV, 1e20/u.m**3, 'p', coulomb_log = 20)
    <Quantity 5812633.74935004 1 / s>

    """
    from plasmapy.physics.transport.collisions import Coulomb_logarithm
    T_e = T_e.to(u.K, equivalencies=u.temperature_energy())
    if V is not None:
        V = V
    else:
        # electron thermal velocity (most probable)
        V = np.sqrt(2 * k_B * T_e / m_e)

    if coulomb_log is not None:
        coulomb_log_val = coulomb_log
    else:
        particles = ['e', ion_particle]
        coulomb_log_val = Coulomb_logarithm(T_e,
                                            n_e,
                                            particles,
                                            V,
                                            method=coulomb_log_method)
    # this is the same as b_perp in collisions.py, using most probable thermal velocity for V
    # and using ion mass instead of reduced mass
    bperp = e**2 / (4 * np.pi * eps0 * m_e * V**2)
    # collisional cross-section
    sigma = np.pi * (2 * bperp)**2
    # collisional frequency with Coulomb logarithm to correct for small angle collisions
    nu = n_e * sigma * V * coulomb_log_val
    # this coefficient is the constant that pops out when comparing this definition of
    # collisional frequency to the one in collisions.py
    coeff = 4 / np.sqrt(np.pi) / 3
    # collisional frequency modified by the constant difference
    nu_e = coeff * nu
    return nu_e.to(1 / u.s)
Example #19
0
        )

    return np.min(probe_characteristic.current)


@validate_quantities(
    ion_saturation_current={
        "can_be_negative": True,
        "can_be_inf": False,
        "can_be_nan": False,
    },
    T_e={
        "can_be_negative": False,
        "can_be_inf": False,
        "can_be_nan": False,
        "equivalencies": u.temperature_energy(),
    },
    probe_area={"can_be_negative": False, "can_be_inf": False, "can_be_nan": False},
    validations_on_return={"can_be_negative": False},
)
def get_ion_density_LM(
    ion_saturation_current: u.A, T_e: u.eV, probe_area: u.m ** 2, gas
) -> u.m ** -3:
    r"""Implement the Langmuir-Mottley (LM) method of obtaining the ion
    density.

    Parameters
    ----------
    ion_saturation_current : ~astropy.units.Quantity
        The ion saturation current in units convertible to A.
Example #20
0
 def T_e(self) -> u.K:
     """Return the electron temperature."""
     if self._T_e is None:
         raise AtomicError("No electron temperature has been specified.")
     return self._T_e.to(u.K, equivalencies=u.temperature_energy())
Example #21
0
class IonizationState:
    """
    Representation of the ionization state distribution of a single
    element or isotope.

    Parameters
    ----------
    particle: str, integer, or ~plasmapy.particles.Particle
        A `str` or `~plasmapy.particles.Particle` instance representing
        an element or isotope, or an integer representing the atomic
        number of an element.

    ionic_fractions: ~numpy.ndarray, list, tuple, or ~astropy.units.Quantity; optional
        The ionization fractions of an element, where the indices
        correspond to integer charge.  This argument should contain the
        atomic number plus one items, and must sum to one within an
        absolute tolerance of ``tol`` if dimensionless.  Alternatively,
        this argument may be a `~astropy.units.Quantity` that represents
        the number densities of each neutral/ion.

    T_e: ~astropy.units.Quantity, keyword-only, optional
        The electron temperature or thermal energy per particle.

    n_elem: ~astropy.units.Quantity, keyword-only, optional
        The number density of the element, including neutrals and all
        ions.

    tol: float or integer, keyword-only, optional
        The absolute tolerance used by `~numpy.isclose` when testing
        normalizations and making comparisons.  Defaults to ``1e-15``.

    Raises
    ------
    ~plasmapy.utils.AtomicError
        If the ionic fractions are not normalized or contain invalid
        values, or if number density information is provided through
        both ``ionic_fractions`` and ``n_elem``.

    ~plasmapy.utils.InvalidParticleError
        If the particle is invalid.

    Examples
    --------
    >>> states = IonizationState('H', [0.6, 0.4], n_elem=1*u.cm**-3, T_e=11000*u.K)
    >>> states.ionic_fractions[0]  # fraction of hydrogen that is neutral
    0.6
    >>> states.ionic_fractions[1]  # fraction of hydrogen that is ionized
    0.4
    >>> states.n_e  # electron number density
    <Quantity 400000. 1 / m3>
    >>> states.n_elem  # element number density
    <Quantity 1000000. 1 / m3>

    Notes
    -----
    Calculation of collisional ionization equilibrium has not yet been
    implemented.

    """

    # TODO: Allow this class to (optionally?) handle negatively charged
    # TODO: ions.  There are instances where singly negatively charged
    # TODO: ions are important in astrophysical plasmas, such as H- in
    # TODO: the atmospheres of relatively cool stars.  There may be some
    # TODO: rare situations where doubly negatively charged ions show up
    # TODO: too, but triply negatively charged ions are very unlikely.

    # TODO: Add in functionality to find equilibrium ionization states.

    @validate_quantities(T_e={"equivalencies": u.temperature_energy()})
    @particle_input(require="element", exclude="ion")
    def __init__(
        self,
        particle: Particle,
        ionic_fractions=None,
        *,
        T_e: u.K = np.nan * u.K,
        kappa: Real = np.inf,
        n_elem: u.m**-3 = np.nan * u.m**-3,
        tol: Union[float, int] = 1e-15,
    ):
        """Initialize an `~plasmapy.particles.IonizationState` instance."""

        self._particle_instance = particle

        try:
            self.tol = tol
            self.T_e = T_e
            self.kappa = kappa

            if (not np.isnan(n_elem)
                    and isinstance(ionic_fractions, u.Quantity)
                    and ionic_fractions.si.unit == u.m**-3):
                raise AtomicError(
                    "Cannot simultaneously provide number density "
                    "through both n_elem and ionic_fractions.")

            self.n_elem = n_elem
            self.ionic_fractions = ionic_fractions

            if ionic_fractions is None and not np.isnan(self.T_e):
                warnings.warn(
                    "Collisional ionization equilibration has not yet "
                    "been implemented in IonizationState; cannot set "
                    "ionic fractions.")

        except Exception as exc:
            raise AtomicError(f"Unable to create IonizationState instance for "
                              f"{particle.particle}.") from exc

    def __str__(self) -> str:
        return f"<IonizationState instance for {self.base_particle}>"

    def __repr__(self) -> str:
        return self.__str__()

    def __getitem__(self, value) -> State:
        """Return information for a single ionization level."""
        if isinstance(value, slice):
            raise TypeError("IonizationState instances cannot be sliced.")

        if isinstance(value, Integral) and 0 <= value <= self.atomic_number:
            result = State(
                value,
                self.ionic_fractions[value],
                self.ionic_symbols[value],
                self.number_densities[value],
            )
        else:
            if not isinstance(value, Particle):
                try:
                    value = Particle(value)
                except InvalidParticleError as exc:
                    raise InvalidParticleError(
                        f"{value} is not a valid integer charge or "
                        f"particle.") from exc

            same_element = value.element == self.element
            same_isotope = value.isotope == self.isotope
            has_charge_info = value.is_category(
                any_of=["charged", "uncharged"])

            if same_element and same_isotope and has_charge_info:
                Z = value.integer_charge
                result = State(
                    Z,
                    self.ionic_fractions[Z],
                    self.ionic_symbols[Z],
                    self.number_densities[Z],
                )
            else:
                if not same_element or not same_isotope:
                    raise AtomicError("Inconsistent element or isotope.")
                elif not has_charge_info:
                    raise ChargeError("No integer charge provided.")
        return result

    def __setitem__(self, key, value):
        raise NotImplementedError(
            "Item assignment of an IonizationState instance is not "
            "allowed because the ionic fractions for different "
            "ionization levels must be set simultaneously due to the "
            "normalization constraint.")

    def __iter__(self):
        """Initialize an instance prior to iteration."""
        self._charge_index = 0
        return self

    def __next__(self):
        """
        Return a `~plasmapy.particles.State` instance that contains
        information about a particular ionization level.
        """
        if self._charge_index <= self.atomic_number:
            result = State(
                self._charge_index,
                self._ionic_fractions[self._charge_index],
                self.ionic_symbols[self._charge_index],
                self.number_densities[self._charge_index],
            )
            self._charge_index += 1
            return result
        else:
            del self._charge_index
            raise StopIteration

    def __eq__(self, other):
        """
        Return `True` if the ionic fractions, number density scaling
        factor (if set), and electron temperature (if set) are all
        equal, and `False` otherwise.

        Raises
        ------
        TypeError
            If ``other`` is not an `~plasmapy.particles.IonizationState`
            instance.

        AtomicError
            If ``other`` corresponds to a different element or isotope.

        Examples
        --------
        >>> IonizationState('H', [1, 0], tol=1e-6) == IonizationState('H', [1, 1e-6], tol=1e-6)
        True
        >>> IonizationState('H', [1, 0], tol=1e-8) == IonizationState('H', [1, 1e-6], tol=1e-5)
        False

        """
        if not isinstance(other, IonizationState):
            raise TypeError(
                "An instance of the IonizationState class may only be "
                "compared with another IonizationState instance.")

        same_element = self.element == other.element
        same_isotope = self.isotope == other.isotope

        if not same_element or not same_isotope:
            raise AtomicError(
                "An instance of the IonizationState class may only be "
                "compared with another IonizationState instance if "
                "both correspond to the same element and/or isotope.")

        # Use the tighter of the two tolerances. For thermodynamic
        # quantities, use it as a relative tolerance because the values
        # may substantially depart from order unity.

        min_tol = np.min([self.tol, other.tol])

        same_T_e = (np.isnan(self.T_e) and np.isnan(other.T_e) or u.allclose(
            self.T_e, other.T_e, rtol=min_tol * u.K, atol=0 * u.K))

        same_n_elem = (np.isnan(self.n_elem) and np.isnan(other.n_elem)
                       or u.allclose(self.n_elem,
                                     other.n_elem,
                                     rtol=min_tol * u.m**-3,
                                     atol=0 * u.m**-3))

        # For the next line, recall that np.nan == np.nan is False (sigh)

        same_fractions = np.any([
            np.allclose(self.ionic_fractions,
                        other.ionic_fractions,
                        rtol=0,
                        atol=min_tol),
            np.all(np.isnan(self.ionic_fractions))
            and np.all(np.isnan(other.ionic_fractions)),
        ])

        return np.all([
            same_element, same_isotope, same_T_e, same_n_elem, same_fractions
        ])

    @property
    def ionic_fractions(self) -> np.ndarray:
        """
        Return the ionic fractions, where the index corresponds to
        the integer charge.

        Examples
        --------
        >>> hydrogen_states = IonizationState('H', [0.9, 0.1])
        >>> hydrogen_states.ionic_fractions
        array([0.9, 0.1])

        """
        return self._ionic_fractions

    @ionic_fractions.setter
    def ionic_fractions(self, fractions):
        """
        Set the ionic fractions, while checking that the new values are
        valid and normalized to one.
        """
        if fractions is None or np.all(np.isnan(fractions)):
            self._ionic_fractions = np.full(self.atomic_number + 1,
                                            np.nan,
                                            dtype=np.float64)
            return

        try:
            if np.min(fractions) < 0:
                raise AtomicError("Cannot have negative ionic fractions.")

            if len(fractions) != self.atomic_number + 1:
                raise AtomicError("The length of ionic_fractions must be "
                                  f"{self.atomic_number + 1}.")

            if isinstance(fractions, u.Quantity):
                fractions = fractions.to(u.m**-3)
                self.n_elem = np.sum(fractions)
                self._ionic_fractions = np.array(fractions / self.n_elem)
            else:
                fractions = np.array(fractions, dtype=np.float64)
                sum_of_fractions = np.sum(fractions)
                all_nans = np.all(np.isnan(fractions))

                if not all_nans:
                    if np.any(fractions < 0) or np.any(fractions > 1):
                        raise AtomicError(
                            "Ionic fractions must be between 0 and 1.")

                    if not np.isclose(
                            sum_of_fractions, 1, rtol=0, atol=self.tol):
                        raise AtomicError("Ionic fractions must sum to one.")

                self._ionic_fractions = fractions

        except Exception as exc:
            raise AtomicError(
                f"Unable to set ionic fractions of {self.element} "
                f"to {fractions}.") from exc

    def _is_normalized(self, tol: Optional[Real] = None) -> bool:
        """
        Return `True` if the sum of the ionization fractions is equal to
        one within the allowed tolerance, and `False` otherwise.
        """
        tol = tol if tol is not None else self.tol
        if not isinstance(tol, Real):
            raise TypeError("tol must be an int or float.")
        if not 0 <= tol < 1:
            raise ValueError("Need 0 <= tol < 1.")
        total = np.sum(self._ionic_fractions)
        return np.isclose(total, 1, atol=tol, rtol=0)

    def normalize(self) -> None:
        """
        Normalize the ionization state distribution (if set) so that the
        sum becomes equal to one.
        """
        self._ionic_fractions = self._ionic_fractions / np.sum(
            self._ionic_fractions)

    @property
    def equil_ionic_fractions(self, T_e: u.K = None):
        """
        Return the equilibrium ionic fractions for temperature ``T_e``
        or the temperature set in the IonizationState instance.  Not
        implemented.
        """
        raise NotImplementedError

    @validate_quantities(equivalencies=u.temperature_energy())
    def equilibrate(self, T_e: u.K = np.nan * u.K):
        """
        Set the ionic fractions to collisional ionization equilibrium
        for temperature ``T_e``.  Not implemented.
        """
        # self.ionic_fractions = self.equil_ionic_fractions
        raise NotImplementedError

    @property
    @validate_quantities
    def n_e(self) -> u.m**-3:
        """
        Return the electron number density assuming a single species
        plasma.
        """
        return np.sum(self._n_elem * self.ionic_fractions *
                      self.integer_charges)

    @property
    @validate_quantities
    def n_elem(self) -> u.m**-3:
        """Return the total number density of neutrals and all ions."""
        return self._n_elem.to(u.m**-3)

    @n_elem.setter
    @validate_quantities
    def n_elem(self, value: u.m**-3):
        """Set the number density of neutrals and all ions."""
        if value < 0 * u.m**-3:
            raise AtomicError
        if 0 * u.m**-3 < value <= np.inf * u.m**-3:
            self._n_elem = value.to(u.m**-3)
        elif np.isnan(value):
            self._n_elem = np.nan * u.m**-3

    @property
    @validate_quantities
    def number_densities(self) -> u.m**-3:
        """Return the number densities for each state."""
        try:
            return (self.n_elem * self.ionic_fractions).to(u.m**-3)
        except Exception:
            return np.full(self.atomic_number + 1, np.nan) * u.m**-3

    @number_densities.setter
    @validate_quantities
    def number_densities(self, value: u.m**-3):
        """Set the number densities for each state."""
        if np.any(value.value < 0):
            raise AtomicError("Number densities cannot be negative.")
        if len(value) != self.atomic_number + 1:
            raise AtomicError(f"Incorrect number of charge states for "
                              f"{self.base_particle}")
        value = value.to(u.m**-3)

        self._n_elem = value.sum()
        self._ionic_fractions = value / self._n_elem

    @property
    @validate_quantities(equivalencies=u.temperature_energy())
    def T_e(self) -> u.K:
        """Return the electron temperature."""
        if self._T_e is None:
            raise AtomicError("No electron temperature has been specified.")
        return self._T_e.to(u.K, equivalencies=u.temperature_energy())

    @T_e.setter
    @validate_quantities(equivalencies=u.temperature_energy())
    def T_e(self, value: u.K):
        """Set the electron temperature."""
        try:
            value = value.to(u.K, equivalencies=u.temperature_energy())
        except (AttributeError, u.UnitsError, u.UnitConversionError):
            raise AtomicError("Invalid temperature.") from None
        else:
            if value < 0 * u.K:
                raise AtomicError("T_e cannot be negative.")
        self._T_e = value

    @property
    def kappa(self) -> np.real:
        """
        Return the kappa parameter for a kappa distribution function
        for electrons.

        The value of ``kappa`` must be greater than ``1.5`` in order to
        have a valid distribution function.  If ``kappa`` equals
        `~numpy.inf`, then the distribution function reduces to a
        Maxwellian.

        """
        return self._kappa

    @kappa.setter
    def kappa(self, value: Real):
        """
        Set the kappa parameter for a kappa distribution function for
        electrons.  The value must be between ``1.5`` and `~numpy.inf`.
        """
        kappa_errmsg = "kappa must be a real number greater than 1.5"
        if not isinstance(value, Real):
            raise TypeError(kappa_errmsg)
        if value <= 1.5:
            raise ValueError(kappa_errmsg)
        self._kappa = np.real(value)

    @property
    def element(self) -> str:
        """Return the atomic symbol of the element."""
        return self._particle_instance.element

    @property
    def isotope(self) -> Optional[str]:
        """
        Return the isotope symbol for an isotope, or `None` if the
        particle is not an isotope.
        """
        return self._particle_instance.isotope

    @property
    def base_particle(self) -> str:
        """Return the symbol of the element or isotope."""
        return self.isotope if self.isotope else self.element

    @property
    def atomic_number(self) -> int:
        """Return the atomic number of the element."""
        return self._particle_instance.atomic_number

    @property
    def _particle_instances(self) -> List[Particle]:
        """
        Return a list of the `~plasmapy.particles.Particle` class
        instances corresponding to each ion.
        """
        return [
            Particle(self._particle_instance.particle, Z=i)
            for i in range(self.atomic_number + 1)
        ]

    @property
    def ionic_symbols(self) -> List[str]:
        """Return the ionic symbols for all charge states."""
        return [particle.ionic_symbol for particle in self._particle_instances]

    @property
    def integer_charges(self) -> np.ndarray:
        """Return an array with the integer charges."""
        return np.arange(0, self.atomic_number + 1, dtype=np.int)

    @property
    def Z_mean(self) -> np.float64:
        """Return the mean integer charge"""
        if np.nan in self.ionic_fractions:
            raise ChargeError(
                "Z_mean cannot be found because no ionic fraction "
                f"information is available for {self.base_particle}.")
        return np.sum(self.ionic_fractions * np.arange(self.atomic_number + 1))

    @property
    def Z_rms(self) -> np.float64:
        """Return the root mean square integer charge."""
        return np.sqrt(
            np.sum(self.ionic_fractions *
                   np.arange(self.atomic_number + 1)**2))

    @property
    def Z_most_abundant(self) -> List[Integral]:
        """
        Return a `list` of the integer charges with the highest ionic
        fractions.

        Examples
        --------
        >>> He = IonizationState('He', [0.2, 0.5, 0.3])
        >>> He.Z_most_abundant
        [1]
        >>> Li = IonizationState('Li', [0.4, 0.4, 0.2, 0.0])
        >>> Li.Z_most_abundant
        [0, 1]

        """
        if np.any(np.isnan(self.ionic_fractions)):
            raise AtomicError(
                f"Cannot find most abundant ion of {self.base_particle} "
                f"because the ionic fractions have not been defined.")

        return np.flatnonzero(
            self.ionic_fractions == self.ionic_fractions.max()).tolist()

    @property
    def tol(self) -> Real:
        """Return the absolute tolerance for comparisons."""
        return self._tol

    @tol.setter
    def tol(self, atol: Real):
        """Set the absolute tolerance for comparisons."""
        if not isinstance(atol, Real):
            raise TypeError("The attribute tol must be a real number.")
        if 0 <= atol < 1:
            self._tol = atol
        else:
            raise ValueError("Need 0 <= tol < 1.")

    def _get_states_info(self, minimum_ionic_fraction=0.01) -> List[str]:
        """
        Return a `list` containing the ion symbol, ionic fraction, and
        (if available) the number density for that ion.
        """

        states_info = []

        for state in self:
            if state.ionic_fraction > minimum_ionic_fraction:
                state_info = ""
                symbol = state.ionic_symbol
                if state.integer_charge < 10:
                    symbol = symbol[:-2] + " " + symbol[-2:]
                fraction = "{:.3f}".format(state.ionic_fraction)

                state_info += f"{symbol}: {fraction}"

                if np.isfinite(self.n_elem):
                    value = "{:.2e}".format(state.number_density.si.value)
                    state_info += f"    n_i = {value} m**-3"

                states_info.append(state_info)

        return states_info

    def info(self, minimum_ionic_fraction: Real = 0.01) -> None:
        """
        Print quicklook information for an
        `~plasmapy.particles.IonizationState` instance.

        Parameters
        ----------
        minimum_ionic_fraction: Real
            If the ionic fraction for a particular ionization state is
            below this level, then information for it will not be
            printed.  Defaults to 0.01.

        Example
        -------
        >>> He_states = IonizationState(
        ...     'He',
        ...     [0.941, 0.058, 0.001],
        ...     T_e = 5.34 * u.K,
        ...     kappa = 4.05,
        ...     n_elem = 5.51e19 * u.m ** -3,
        ... )
        >>> He_states.info()
        IonizationState instance for He with Z_mean = 0.06
        ----------------------------------------------------------------
        He  0+: 0.941    n_i = 5.18e+19 m**-3
        He  1+: 0.058    n_i = 3.20e+18 m**-3
        ----------------------------------------------------------------
        n_elem = 5.51e+19 m**-3
        n_e = 3.31e+18 m**-3
        T_e = 5.34e+00 K
        kappa = 4.05
        ----------------------------------------------------------------

        """
        separator_line = [64 * "-"]

        scientific = "{:.2e}"
        floaty = "{:.2f}"

        n_elem = scientific.format(self.n_elem.value)
        n_e = scientific.format(self.n_e.value)
        T_e = scientific.format(self.T_e.value)
        kappa = floaty.format(self.kappa)
        Z_mean = floaty.format(self.Z_mean)

        output = [
            f"IonizationState instance for {self.base_particle} with Z_mean = {Z_mean}"
        ]
        attributes = []

        if not np.all(np.isnan(self.ionic_fractions)):
            output += separator_line
            output += self._get_states_info(minimum_ionic_fraction)
            output += separator_line

        if not np.isnan(self.n_elem):
            attributes.append(f"n_elem = {n_elem} m**-3")
            attributes.append(f"n_e = {n_e} m**-3")
        if not np.isnan(self.T_e):
            attributes.append(f"T_e = {T_e} K")
        if np.isfinite(self.kappa):
            attributes.append(f"kappa = {kappa}")

        if attributes:
            attributes += separator_line
            output += attributes

        for line in output:
            print(line)
Example #22
0
def kappa_velocity_1D(v,
                      T,
                      kappa,
                      particle="e",
                      V_drift=0,
                      vTh=np.nan,
                      units="units"):
    r"""
    Return the probability at the velocity `v` in m/s
    to find a particle `particle` in a plasma of temperature `T`
    following the Kappa distribution function. The slope of the
    tail of the Kappa distribution function is set by 'kappa', which
    must be greater than :math:`1/2`.

    Parameters
    ----------
    v: ~astropy.units.Quantity
        The velocity in units convertible to m/s.

    T: ~astropy.units.Quantity
        The temperature in Kelvin.

    kappa: ~astropy.units.Quantity
        The kappa parameter is a dimensionless number which sets the slope
        of the energy spectrum of suprathermal particles forming the tail
        of the Kappa velocity distribution function. Kappa must be greater
        than :math:`3/2`.

    particle: str, optional
        Representation of the particle species(e.g., `'p` for protons, `'D+'`
        for deuterium, or `'He-4 +1'` for :math:`He_4^{+1}`
        (singly ionized helium-4), which defaults to electrons.

    V_drift: ~astropy.units.Quantity, optional
        The drift velocity in units convertible to m/s.

    vTh: ~astropy.units.Quantity, optional
        Thermal velocity (most probable) in m/s. This is used for
        optimization purposes to avoid re-calculating `vTh`, for example
        when integrating over velocity-space.

    units: str, optional
        Selects whether to run function with units and unit checks (when
        equal to "units") or to run as unitless (when equal to "unitless").
        The unitless version is substantially faster for intensive
        computations.

    Returns
    -------
    f : ~astropy.units.Quantity
        probability in Velocity^-1, normalized so that
        :math:`\int_{-\infty}^{+\infty} f(v) dv = 1`.

    Raises
    ------
    TypeError
        A parameter argument is not a `~astropy.units.Quantity` and
        cannot be converted into a `~astropy.units.Quantity`.

    ~astropy.units.UnitConversionError
        If the parameters is not in appropriate units.

    ValueError
        If the temperature is negative, or the particle mass or charge state
        cannot be found.

    Notes
    -----
    In one dimension, the Kappa velocity distribution function describing
    the distribution of particles with speed :math:`v` in a plasma with
    temperature :math:`T` and suprathermal parameter :math:`\kappa` is
    given by:

    .. math::

       f = A_\kappa \left(1 + \frac{(\vec{v} -
       \vec{V_{drift}})^2}{\kappa v_Th,\kappa^2}\right)^{-\kappa}

    where :math:`v_Th,\kappa` is the kappa thermal speed
    and :math:`A_\kappa = \frac{1}{\sqrt{\pi} \kappa^{3/2} v_Th,\kappa^2
    \frac{\Gamma(\kappa + 1)}{\Gamma(\kappa - 1/2)}}`
    is the normalization constant.

    As :math:`\kappa` approaches infinity, the kappa distribution function
    converges to the Maxwellian distribution function.

    Examples
    --------
    >>> from plasmapy.physics import kappa_velocity_1D
    >>> from astropy import units as u
    >>> v=1*u.m/u.s
    >>> kappa_velocity_1D(v=v, T=30000*u.K, kappa=4, particle='e',V_drift=0*u.m/u.s)
    <Quantity 6.75549854e-07 s / m>
    """
    # must have kappa > 3/2 for distribution function to be valid
    if kappa <= 3 / 2:
        raise ValueError(f"Must have kappa > 3/2, instead of {kappa}.")
    if units == "units":
        # unit checks and conversions
        # checking velocity units
        v = v.to(u.m / u.s)
        # catching case where drift velocities have default values, they
        # need to be assigned units
        if V_drift == 0:
            if not isinstance(V_drift, astropy.units.quantity.Quantity):
                V_drift = V_drift * u.m / u.s
        # checking units of drift velocities
        V_drift = V_drift.to(u.m / u.s)
        # convert temperature to Kelvins
        T = T.to(u.K, equivalencies=u.temperature_energy())
        if np.isnan(vTh):
            # get thermal velocity and thermal velocity squared
            vTh = kappa_thermal_speed(T,
                                      kappa,
                                      particle=particle)
        elif not np.isnan(vTh):
            # check units of thermal velocity
            vTh = vTh.to(u.m / u.s)
    elif np.isnan(vTh) and units == "unitless":
        # assuming unitless temperature is in Kelvins
        vTh = (kappa_thermal_speed(T * u.K,
                                   kappa,
                                   particle=particle)).si.value
    # Get thermal velocity squared and accounting for 1D instead of 3D
    vThSq = vTh ** 2
    # Get square of relative particle velocity
    vSq = (v - V_drift) ** 2
    # calculating distribution function
    expTerm = (1 + vSq / (kappa * vThSq)) ** (-kappa)
    coeff1 = 1 / (np.sqrt(np.pi) * kappa ** (3 / 2) * vTh)
    coeff2 = gamma(kappa + 1) / (gamma(kappa - 1 / 2))
    distFunc = coeff1 * coeff2 * expTerm
    if units == "units":
        return distFunc.to(u.s / u.m)
    elif units == "unitless":
        return distFunc
Example #23
0
def kappa_velocity_3D(vx,
                      vy,
                      vz,
                      T,
                      kappa,
                      particle="e",
                      Vx_drift=0,
                      Vy_drift=0,
                      Vz_drift=0,
                      vTh=np.nan,
                      units="units"):
    r"""
    Return the probability of finding a particle with velocity components
    `v_x`, `v_y`, and `v_z`in m/s in a suprathermal plasma of temperature
    `T` and parameter 'kappa' which follows the 3D Kappa distribution function.
    This function assumes Cartesian coordinates.

    Parameters
    ----------
    vx: ~astropy.units.Quantity
        The velocity in x-direction units convertible to m/s.

    vy: ~astropy.units.Quantity
        The velocity in y-direction units convertible to m/s.

    vz: ~astropy.units.Quantity
        The velocity in z-direction units convertible to m/s.

    T: ~astropy.units.Quantity
        The temperature, preferably in Kelvin.

    kappa: ~astropy.units.Quantity
        The kappa parameter is a dimensionless number which sets the slope
        of the energy spectrum of suprathermal particles forming the tail
        of the Kappa velocity distribution function. Kappa must be greater
        than :math:`3/2`.

    particle: str, optional
        Representation of the particle species(e.g., 'p' for protons, 'D+'
        for deuterium, or 'He-4 +1' for :math:`He_4^{+1}` : singly ionized
        helium-4), which defaults to electrons.

    Vx_drift: ~astropy.units.Quantity, optional
        The drift velocity in x-direction units convertible to m/s.

    Vy_drift: ~astropy.units.Quantity, optional
        The drift velocity in y-direction units convertible to m/s.

    Vz_drift: ~astropy.units.Quantity, optional
        The drift velocity in z-direction units convertible to m/s.

    vTh: ~astropy.units.Quantity, optional
        Thermal velocity (most probable) in m/s. This is used for
        optimization purposes to avoid re-calculating `vTh`, for example
        when integrating over velocity-space.

    units: str, optional
        Selects whether to run function with units and unit checks (when
        equal to "units") or to run as unitless (when equal to "unitless").
        The unitless version is substantially faster for intensive
        computations.

    Returns
    -------
    f : ~astropy.units.Quantity
        probability in Velocity^-1, normalized so that:
        :math:`\iiint_{0}^{\infty} f(\vec{v}) d\vec{v} = 1`

    Raises
    ------
    TypeError
        The parameter arguments are not Quantities and
        cannot be converted into Quantities.

    ~astropy.units.UnitConversionError
        If the parameters is not in appropriate units.

    ValueError
        If the temperature is negative, or the particle mass or charge state
        cannot be found.

    Notes
    -----
    In three dimensions, the Kappa velocity distribution function describing
    the distribution of particles with speed :math:`v` in a plasma with
    temperature :math:`T` and suprathermal parameter :math:`\kappa` is given by:

    .. math::

       f = A_\kappa \left(1 + \frac{(\vec{v} -
       \vec{V_{drift}})^2}{\kappa v_Th,\kappa^2}\right)^{-(\kappa + 1)}

    where :math:`v_Th,\kappa` is the kappa thermal speed
    and :math:`A_\kappa = \frac{1}{2 \pi (\kappa v_Th,\kappa^2)^{3/2}}
    \frac{\Gamma(\kappa + 1)}{\Gamma(\kappa - 1/2) \Gamma(3/2)}` is the
    normalization constant.

    As :math:`\kappa` approaches infinity, the kappa distribution function
    converges to the Maxwellian distribution function.

    See also
    --------
    kappa_velocity_1D
    kappa_thermal_speed

    Example
    -------
    >>> from plasmapy.physics import kappa_velocity_3D
    >>> from astropy import units as u
    >>> v=1*u.m/u.s
    >>> kappa_velocity_3D(vx=v,
    ... vy=v,
    ... vz=v,
    ... T=30000*u.K,
    ... kappa=4,
    ... particle='e',
    ... Vx_drift=0*u.m/u.s,
    ... Vy_drift=0*u.m/u.s,
    ... Vz_drift=0*u.m/u.s)
    <Quantity 3.7833988e-19 s3 / m3>
    """
    # must have kappa > 3/2 for distribution function to be valid
    if kappa <= 3 / 2:
        raise ValueError(f"Must have kappa > 3/2, instead of {kappa}.")
    if units == "units":
        # unit checks and conversions
        # checking velocity units
        vx = vx.to(u.m / u.s)
        vy = vy.to(u.m / u.s)
        vz = vz.to(u.m / u.s)
        # catching case where drift velocities have default values, they
        # need to be assigned units
        if Vx_drift == 0:
            if not isinstance(Vx_drift, astropy.units.quantity.Quantity):
                Vx_drift = Vx_drift * u.m / u.s
        if Vy_drift == 0:
            if not isinstance(Vy_drift, astropy.units.quantity.Quantity):
                Vy_drift = Vy_drift * u.m / u.s
        if Vz_drift == 0:
            if not isinstance(Vz_drift, astropy.units.quantity.Quantity):
                Vz_drift = Vz_drift * u.m / u.s
        # checking units of drift velocities
        Vx_drift = Vx_drift.to(u.m / u.s)
        Vy_drift = Vy_drift.to(u.m / u.s)
        Vz_drift = Vz_drift.to(u.m / u.s)
        # convert temperature to Kelvins
        T = T.to(u.K, equivalencies=u.temperature_energy())
        if np.isnan(vTh):
            # get thermal velocity and thermal velocity squared
            vTh = kappa_thermal_speed(T,
                                      kappa,
                                      particle=particle)
        elif not np.isnan(vTh):
            # check units of thermal velocity
            vTh = vTh.to(u.m / u.s)
    elif np.isnan(vTh) and units == "unitless":
        # assuming unitless temperature is in Kelvins
        vTh = (kappa_thermal_speed(T * u.K,
                                   kappa,
                                   particle=particle)).si.value
    # getting square of thermal velocity
    vThSq = vTh ** 2
    # Get square of relative particle velocity
    vSq = ((vx - Vx_drift) ** 2 + (vy - Vy_drift) ** 2 + (vz - Vz_drift) ** 2)
    # calculating distribution function
    expTerm = (1 + vSq / (kappa * vThSq)) ** (-(kappa + 1))
    coeff1 = 1 / (2 * np.pi * (kappa * vThSq) ** (3 / 2))
    coeff2 = gamma(kappa + 1) / (gamma(kappa - 1 / 2) * gamma(3 / 2))
    distFunc = coeff1 * coeff2 * expTerm
    if units == "units":
        return distFunc.to((u.s / u.m)**3)
    elif units == "unitless":
        return distFunc
Example #24
0
def Debye_length(T_e, n_e):
    r"""Calculate the characteristic decay length for electric fields,
     due to charge screening.

    Parameters
    ----------
    T_e: ~astropy.units.Quantity
        Electron temperature

    n_e: ~astropy.units.Quantity
        Electron number density

    Returns
    -------
    lambda_D : ~astropy.units.Quantity
        The Debye length in meters

    Raises
    ------
    TypeError
        If either argument is not a `~astropy.units.Quantity`

    ~astropy.units.UnitConversionError
        If either argument is in incorrect units

    ValueError
        If either argument contains invalid values

    Warns
    -----
    ~astropy.units.UnitsWarning
        If units are not provided, SI units are assumed

    Notes
    -----
    The Debye length is the exponential scale length for charge
    screening and is given by

    .. math::
        \lambda_D = \sqrt{\frac{\epsilon_0 k_b T_e}{n_e e^2}}

    for an electron plasma with nearly stationary ions.

    The electrical potential will drop by a factor of 1/e every Debye
    length.

    Plasmas will generally be quasineutral on length scales significantly
    larger than the Debye length.

    See Also
    --------
    Debye_number

    Example
    -------
    >>> from astropy import units as u
    >>> Debye_length(5e6*u.K, 5e15*u.m**-3)
    <Quantity 0.00218226 m>

    """

    T_e = T_e.to(u.K, equivalencies=u.temperature_energy())
    lambda_D = np.sqrt(eps0 * k_B * T_e / (n_e * e**2))
    return lambda_D.to(u.m)
Example #25
0
def thermal_speed(T, particle="e-", method="most_probable"):
    r"""
    Return the most probable speed for a particle within a Maxwellian
    distribution.

    Parameters
    ----------
    T : ~astropy.units.Quantity
        The particle temperature in either kelvin or energy per particle

    particle : str, optional
        Representation of the particle species (e.g., `'p'` for protons, `'D+'`
        for deuterium, or `'He-4 +1'` for singly ionized helium-4),
        which defaults to electrons.  If no charge state information is
        provided, then the particles are assumed to be singly charged.

    method : str, optional
        Method to be used for calculating the thermal speed. Options are
        `'most_probable'` (default), `'rms'`, and `'mean_magnitude'`.

    Returns
    -------
    V : ~astropy.units.Quantity
        particle thermal speed

    Raises
    ------
    TypeError
        The particle temperature is not a ~astropy.units.Quantity

    ~astropy.units.UnitConversionError
        If the particle temperature is not in units of temperature or
        energy per particle

    ValueError
        The particle temperature is invalid or particle cannot be used to
        identify an isotope or particle

    Warns
    -----
    RelativityWarning
        If the ion sound speed exceeds 5% of the speed of light, or

    ~astropy.units.UnitsWarning
        If units are not provided, SI units are assumed.

    Notes
    -----
    The particle thermal speed is given by:

    .. math::
        V_{th,i} = \sqrt{\frac{2 k_B T_i}{m_i}}

    This function yields the most probable speed within a distribution
    function.  However, the definition of thermal velocity varies by
    the square root of two depending on whether or not this velocity
    absorbs that factor in the expression for a Maxwellian
    distribution.  In particular, the expression given in the NRL
    Plasma Formulary [1] is a square root of two smaller than the
    result from this function.

    Examples
    --------
    >>> from astropy import units as u
    >>> thermal_speed(5*u.eV, 'p')
    <Quantity 30949.69018286 m / s>
    >>> thermal_speed(1e6*u.K, particle='p')
    <Quantity 128486.55193256 m / s>
    >>> thermal_speed(5*u.eV)
    <Quantity 1326205.12123959 m / s>
    >>> thermal_speed(1e6*u.K)
    <Quantity 5505693.98842538 m / s>
    >>> thermal_speed(1e6*u.K, method="rms")
    <Quantity 6743070.47577549 m / s>
    >>> thermal_speed(1e6*u.K, method="mean_magnitude")
    <Quantity 6212510.3969422 m / s>

    """

    T = T.to(u.K, equivalencies=u.temperature_energy())

    try:
        m = atomic.ion_mass(particle)
    except AtomicError:
        raise ValueError("Unable to find {particle} mass in thermal_speed")

    # different methods, as per https://en.wikipedia.org/wiki/Thermal_velocity
    if method == "most_probable":
        V = (np.sqrt(2 * k_B * T / m)).to(u.m / u.s)
    elif method == "rms":
        V = (np.sqrt(3 * k_B * T / m)).to(u.m / u.s)
    elif method == "mean_magnitude":
        V = (np.sqrt(8 * k_B * T / (m * np.pi))).to(u.m / u.s)
    else:
        raise ValueError("Method {method} not supported in thermal_speed")

    return V
Example #26
0
def _check_quantity(arg, argname, funcname, units, can_be_negative=True,
                    can_be_complex=False, can_be_inf=True):
    """
    Raise an exception if an object is not a `~astropy.units.Quantity`
    withmcorrect units and valid numerical values.

    Parameters
    ----------
    arg : ~astropy.units.Quantity
        The object to be tested.

    argname : str
        The name of the argument to be printed in error messages.

    funcname : str
        The name of the original function to be printed in error messages.

    units : ~astropy.units.Unit or list with ~astropy.units.Unit items
        Acceptable units for `arg`.

    can_be_negative : bool, optional
        `True` if the `~astropy.units.Quantity` can be negative,
        `False` otherwise. Defaults to `True`.

    can_be_complex : bool, optional
        `True` if the `~astropy.units.Quantity` can be a complex
        number, `False` otherwise. Defaults to `False`.

    can_be_inf : bool, optional
        `True` if the `~astropy.units.Quantity` can contain infinite
        values, `False` otherwise.  Defaults to `True`.

    Raises
    ------
    TypeError
        If the argument is not a `~astropy.units.Quantity` or units is
        not entirely units.

    ~astropy.units.UnitConversionError
        If the argument is not in acceptable units.

    ValueError
        If the argument contains any `~numpy.nan` or other invalid
        values as determined by the keywords.

    UserWarning
        If a `~astropy.units.Quantity` is not provided and unique units
        are provided, a `UserWarning` will be raised and the inputted
        units will be assumed.

    Examples
    --------
    >>> from astropy import units as u
    >>> _check_quantity(4*u.T, 'B', 'f', u.T)

    """

    # TODO: Replace `funcname` with func.__name__?

    if not isinstance(units, list):
        units = [units]

    for unit in units:
        if not isinstance(unit, (u.Unit, u.CompositeUnit, u.IrreducibleUnit)):
            raise TypeError("The keyword 'units' to check_quantity must be "
                            "a unit or a list/tuple containing only units.")

    # Create a generic error message

    typeerror_message = ("The argument " + argname + " to " + funcname +
                         " should be a Quantity with ")

    if len(units) == 1:
        typeerror_message += "the following units: " + str(units[0])
    else:
        typeerror_message += "one of the following units: "
        for unit in units:
            typeerror_message += str(unit)
            if unit != units[-1]:
                typeerror_message += ", "

    if isinstance(arg, (u.Unit, u.CompositeUnit, u.IrreducibleUnit)):
        raise TypeError(typeerror_message)

    # Make sure arg is a quantity with correct units

    if not isinstance(arg, (u.Quantity)):
        if len(units) != 1:
            raise TypeError(typeerror_message)
        else:
            try:
                arg = arg*units[0]
            except Exception:
                raise TypeError(typeerror_message)
            else:
                raise UserWarning("No units are specified for " + argname +
                                  " in " + funcname + ". Assuming units of " +
                                  str(units[0]) + ".")

    in_acceptable_units = []

    for unit in units:
        try:
            arg.unit.to(unit, equivalencies=u.temperature_energy())
        except Exception:
            in_acceptable_units.append(False)
        else:
            in_acceptable_units.append(True)

    if not np.any(in_acceptable_units):
        raise u.UnitConversionError(typeerror_message)

    # Make sure that the quantity has valid numerical values

    valueerror_message = ("The argument " + argname + " to function " +
                          funcname + " cannot contain ")

    if np.any(np.isnan(arg.value)):
        raise ValueError(valueerror_message + "NaNs.")
    elif np.any(np.iscomplex(arg.value)) and not can_be_complex:
        raise ValueError(valueerror_message + "complex numbers.")
    elif not can_be_negative and np.any(arg.value < 0):
        raise ValueError(valueerror_message + "negative numbers.")
    elif not can_be_inf and np.any(np.isinf(arg.value)):
        raise ValueError(valueerror_message + "infs.")
Example #27
0
def Maxwellian_1D(v,
                  T,
                  particle="e",
                  V_drift=0,
                  vTh=np.nan,
                  units="units"):
    r"""
    Returns the probability at the velocity `v` in m/s
    to find a particle `particle` in a plasma of temperature `T`
    following the Maxwellian distribution function.

    Parameters
    ----------
    v: ~astropy.units.Quantity
        The velocity in units convertible to m/s.

    T: ~astropy.units.Quantity
        The temperature in Kelvin.

    particle: str, optional
        Representation of the particle species(e.g., `'p'` for protons, `'D+'`
        for deuterium, or `'He-4 +1'` for :math:`He_4^{+1}`
        (singly ionized helium-4),
        which defaults to electrons.

    V_drift: ~astropy.units.Quantity, optional
        The drift velocity in units convertible to m/s.

    vTh: ~astropy.units.Quantity, optional
        Thermal velocity (most probable) in m/s. This is used for
        optimization purposes to avoid re-calculating vTh, for example
        when integrating over velocity-space.

    units: str, optional
        Selects whether to run function with units and unit checks (when
        equal to "units") or to run as unitless (when equal to "unitless").
        The unitless version is substantially faster for intensive
        computations.

    Returns
    -------
    f : ~astropy.units.Quantity
        Probability in Velocity^-1, normalized so that
        :math:`\int_{-\infty}^{+\infty} f(v) dv = 1`.

    Raises
    ------
    TypeError
        The parameter arguments are not Quantities and
        cannot be converted into Quantities.

    ~astropy.units.UnitConversionError
        If the parameters are not in appropriate units.

    ValueError
        If the temperature is negative, or the particle mass or charge state
        cannot be found.

    Notes
    -----
    In one dimension, the Maxwellian distribution function for a particle of
    mass m, velocity v, a drift velocity V and with temperature T is:

    .. math::

        f = \sqrt{\frac{m}{2 \pi k_B T}} e^{-\frac{m}{2 k_B T} (v-V)^2}
        f = (\pi * v_Th^2)^{-1/2} e^{-(v - v_{drift})^2 / v_Th^2}

    where :math:`v_Th = \sqrt(2 k_B T / m)` is the thermal speed

    Examples
    --------
    >>> from plasmapy.physics import Maxwellian_1D
    >>> from astropy import units as u
    >>> v=1*u.m/u.s
    >>> Maxwellian_1D(v=v, T= 30000*u.K, particle='e',V_drift=0*u.m/u.s)
    <Quantity 5.91632969e-07 s / m>
    """
    if units == "units":
        # unit checks and conversions
        # checking velocity units
        v = v.to(u.m / u.s)
        # catching case where drift velocities have default values, they
        # need to be assigned units
        if V_drift == 0:
            if not isinstance(V_drift, astropy.units.quantity.Quantity):
                V_drift = V_drift * u.m / u.s
        # checking units of drift velocities
        V_drift = V_drift.to(u.m / u.s)
        # convert temperature to Kelvins
        T = T.to(u.K, equivalencies=u.temperature_energy())
        if np.isnan(vTh):
            # get thermal velocity and thermal velocity squared
            vTh = (thermal_speed(T,
                                 particle=particle,
                                 method="most_probable"))
        elif not np.isnan(vTh):
            # check units of thermal velocity
            vTh = vTh.to(u.m / u.s)
    elif np.isnan(vTh) and units == "unitless":
        # assuming unitless temperature is in Kelvins
        vTh = (thermal_speed(T * u.K,
                             particle=particle,
                             method="most_probable")).si.value
    # Get thermal velocity squared
    vThSq = vTh ** 2
    # Get square of relative particle velocity
    vSq = (v - V_drift) ** 2
    # calculating distribution function
    coeff = (vThSq * np.pi) ** (-1 / 2)
    expTerm = np.exp(-vSq / vThSq)
    distFunc = coeff * expTerm
    if units == "units":
        return distFunc.to(u.s / u.m)
    elif units == "unitless":
        return distFunc
Example #28
0
def Coulomb_logarithm(T, n_e, particles, V=None):
    r"""Estimates the Coulomb logarithm.

    Parameters
    ----------

    T : Quantity
        Temperature in units of temperature or energy per particle,
        which is assumed to be equal for both the test particle and
        the target particle

    n_e : Quantity
        The electron density in units convertible to per cubic meter.

    particles : tuple
        A tuple containing string representations of the test particle
        (listed first) and the target particle (listed second)

    V : Quantity, optional
        The relative velocity between particles.  If not provided,
        thermal velocity is assumed: :math:`\mu V^2 \sim 3 k_B T`
        where `mu` is the reduced mass.

    Returns
    -------
    lnLambda : float or numpy.ndarray
        An estimate of the Coulomb logarithm that is accurate to
        roughly its reciprocal.

    Raises
    ------
    ValueError
        If the mass or charge of either particle cannot be found, or
        any of the inputs contain incorrect values.

    UnitConversionError
        If the units on any of the inputs are incorrect

    UserWarning
        If the inputted velocity is greater than 80% of the speed of
        light.

    TypeError
        If the n_e, T, or V are not Quantities.

    Notes
    -----
    The Coulomb logarithm is given by

    .. math::
        \ln{\Lambda} \equiv \ln\left( \frac{b_{max}}{b_{min}} \right)

    where :math:`b_{min}` and :math:`b_{max}` are the inner and outer
    impact parameters for Coulomb collisions _[1].

    The outer impact parameter is given by the Debye length:
    :math:`b_{min} = \lambda_D` which is a function of electron
    temperature and electron density.  At distances greater than the
    Debye length, electric fields from other particles will be
    screened out due to electrons rearranging themselves.

    The choice of inner impact parameter is more controversial. There
    are two main possibilities.  The first possibility is that the
    inner impact parameter corresponds to a deflection angle of 90
    degrees.  The second possibility is that the inner impact
    parameter is a de Broglie wavelength, :math:`\lambda_B`
    corresponding to the reduced mass of the two particles and the
    relative velocity between collisions.  This function uses the
    standard practice of choosing the inner impact parameter to be the
    maximum of these two possibilities.  Some inconsistencies exist in
    the literature on how to define the inner impact parameter _[2].

    Errors associated with the Coulomb logarithm are of order its
    inverse If the Coulomb logarithm is of order unity, then the
    assumptions made in the standard analysis of Coulomb collisions
    are invalid.

    Examples
    --------
    >>> from astropy import units as u
    >>> n = 1e19*units.m**-3
    >>> T = 1e6*units.K
    >>> particles = ('e', 'p')
    >>> Coulomb_logarithm(T, n, particles)
    14.748259780491056
    >>> Coulomb_logarithm(T, n, particles, 1e6*u.m/u.s)
    11.363478214139432

    References
    ----------
    .. [1] Physics of Fully Ionized Gases, L. Spitzer (1962)

    .. [2] Comparison of Coulomb Collision Rates in the Plasma Physics
    and Magnetically Confined Fusion Literature, W. Fundamenski and
    O.E. Garcia, EFDA–JET–R(07)01
    (http://www.euro-fusionscipub.org/wp-content/uploads/2014/11/EFDR07001.pdf)

    """

    if not isinstance(particles, (list, tuple)) or len(particles) != 2:
        raise ValueError("The third input of Coulomb_logarithm must be a list "
                         "or tuple containing representations of two charged "
                         "particles.")

    masses = np.zeros(2) * units.kg
    charges = np.zeros(2) * units.C

    for particle, i in zip(particles, range(2)):

        try:
            masses[i] = ion_mass(particles[i])
        except Exception:
            raise ValueError("Unable to find mass of particle: " +
                             str(particles[i]) + " in Coulomb_logarithm.")

        try:
            charges[i] = np.abs(e * charge_state(particles[i]))
            if charges[i] is None:
                raise
        except Exception:
            raise ValueError("Unable to find charge of particle: " +
                             str(particles[i]) + " in Coulomb_logarithm.")

    reduced_mass = masses[0] * masses[1] / (masses[0] + masses[1])

    # The outer impact parameter is the Debye length.  At distances
    # greater than the Debye length, the electrostatic potential of a
    # single particle is screened out by the electrostatic potentials
    # of other particles.  Past this distance, the electric fields of
    # individual particles do not affect each other much.  This
    # expression neglects screening by heavier ions.

    T = T.to(units.K, equivalencies=units.temperature_energy())

    b_max = Debye_length(T, n_e)

    # The choice of inner impact parameter is more controversial.
    # There are two broad possibilities, and the expressions in the
    # literature often differ by factors of order unity or by
    # interchanging the reduced mass with the test particle mass.

    # The relative velocity is a source of uncertainty.  It is
    # reasonable to make an assumption relating the thermal energy to
    # the kinetic energy: reduced_mass*velocity**2 is approximately
    # equal to 3*k_B*T.

    # If no relative velocity is inputted, then we make an assumption
    # that relates the thermal energy to the kinetic energy:
    # reduced_mass*velocity**2 is approximately equal to 3*k_B*T.

    if V is None:
        V = np.sqrt(3 * k_B * T / reduced_mass)
    else:
        _check_relativistic(V, 'Coulomb_logarithm', betafrac=0.8)

    # The first possibility is that the inner impact parameter
    # corresponds to a deflection of 90 degrees, which is valid when
    # classical effects dominate.

    b_perp = charges[0] * charges[1] / (4 * pi * eps0 * reduced_mass * V**2)

    # The second possibility is that the inner impact parameter is a
    # de Broglie wavelength.  There remains some ambiguity as to which
    # mass to choose to go into the de Broglie wavelength calculation.
    # Here we use the reduced mass, which will be of the same order as
    # mass of the smaller particle and thus the longer de Broglie
    # wavelength.

    b_deBroglie = hbar / (2 * reduced_mass * V)

    # Coulomb-style collisions will not happen for impact parameters
    # shorter than either of these two impact parameters, so we choose
    # the larger of these two possibilities.

    b_min = np.zeros_like(b_perp)

    for i in range(b_min.size):

        if b_perp.flat[i] > b_deBroglie.flat[i]:
            b_min.flat[i] = b_perp.flat[i]
        else:
            b_min.flat[i] = b_deBroglie.flat[i]

    # Now that we know how many approximations have to go into plasma
    # transport theory, we shall celebrate by returning the Coulomb
    # logarithm.

    ln_Lambda = np.log(b_max / b_min)
    ln_Lambda = ln_Lambda.to(units.dimensionless_unscaled).value

    return ln_Lambda
Example #29
0
def Maxwellian_speed_3D(vx,
                        vy,
                        vz,
                        T,
                        particle="e",
                        Vx_drift=0,
                        Vy_drift=0,
                        Vz_drift=0,
                        vTh=np.nan,
                        units="units"):
    r"""
    Return the probability of finding a particle with speed components
    `vx`, `vy`, and `vz`in m/s in an equilibrium plasma of temperature
    `T` which follows the 3D Maxwellian distribution function. This
    function assumes Cartesian coordinates.

    Parameters
    ----------
    vx: ~astropy.units.Quantity
        The speed in x-direction units convertible to m/s.

    vy: ~astropy.units.Quantity
        The speed in y-direction units convertible to m/s.

    vz: ~astropy.units.Quantity
        The speed in z-direction units convertible to m/s.

    T: ~astropy.units.Quantity
        The temperature, preferably in Kelvin.

    particle: str, optional
        Representation of the particle species(e.g., 'p' for protons, 'D+'
        for deuterium, or 'He-4 +1' for :math:`He_4^{+1}`
        (singly ionized helium-4), which defaults to electrons.

    Vx_drift: ~astropy.units.Quantity
        The drift speed in x-direction units convertible to m/s.

    Vy_drift: ~astropy.units.Quantity
        The drift speed in y-direction units convertible to m/s.

    Vz_drift: ~astropy.units.Quantity
        The drift speed in z-direction units convertible to m/s.

    vTh: ~astropy.units.Quantity, optional
        Thermal velocity (most probable) in m/s. This is used for
        optimization purposes to avoid re-calculating vTh, for example
        when integrating over velocity-space.

    units: str, optional
        Selects whether to run function with units and unit checks (when
        equal to "units") or to run as unitless (when equal to "unitless").
        The unitless version is substantially faster for intensive
        computations.

    Returns
    -------
    f : ~astropy.units.Quantity
        Probability in speed^-1, normalized so that:
        :math:`\iiint_{0}^{\infty} f(\vec{v}) d\vec{v} = 1`.

    Raises
    ------
    TypeError
        A parameter argument is not a `~astropy.units.Quantity` and
        cannot be converted into a `~astropy.units.Quantity`.

    ~astropy.units.UnitConversionError
        If the parameters is not in appropriate units.

    ValueError
        If the temperature is negative, or the particle mass or charge state
        cannot be found.

    Notes
    -----
    In one dimension, the Maxwellian speed distribution function describing
    the distribution of particles with speed :math:`v` in a plasma with
    temperature :math:`T` is given by:

    .. math::

       f = 4 \pi \vec{v}^2 (\pi * v_Th^2)^{-3/2} \exp(-(\vec{v} -
       \vec{V_{drift}})^2 / v_Th^2)

    where :math:`v_Th = \sqrt(2 k_B T / m)` is the thermal speed.

    See also
    --------
    Maxwellian_speed_1D

    Example
    -------
    >>> from plasmapy.physics import Maxwellian_speed_3D
    >>> from astropy import units as u
    >>> v=1*u.m/u.s
    >>> Maxwellian_speed_3D(vx=v,
    ... vy=v,
    ... vz=v,
    ... T=30000*u.K,
    ... particle='e',
    ... Vx_drift=0*u.m/u.s,
    ... Vy_drift=0*u.m/u.s,
    ... Vz_drift=0*u.m/u.s)
    <Quantity 1.76238544e-53 s3 / m3>

    """
    if units == "units":
        # unit checks and conversions
        # checking velocity units
        vx = vx.to(u.m / u.s)
        vy = vy.to(u.m / u.s)
        vz = vz.to(u.m / u.s)
        # catching case where drift velocities have default values, they
        # need to be assigned units
        if Vx_drift == 0:
            if not isinstance(Vx_drift, astropy.units.quantity.Quantity):
                Vx_drift = Vx_drift * u.m / u.s
        if Vy_drift == 0:
            if not isinstance(Vy_drift, astropy.units.quantity.Quantity):
                Vy_drift = Vy_drift * u.m / u.s
        if Vz_drift == 0:
            if not isinstance(Vz_drift, astropy.units.quantity.Quantity):
                Vz_drift = Vz_drift * u.m / u.s
        Vx_drift = Vx_drift.to(u.m / u.s)
        Vy_drift = Vy_drift.to(u.m / u.s)
        Vz_drift = Vz_drift.to(u.m / u.s)
        # convert temperature to Kelvins
        T = T.to(u.K, equivalencies=u.temperature_energy())
        if np.isnan(vTh):
            # get thermal velocity and thermal velocity squared
            vTh = (thermal_speed(T, particle=particle, method="most_probable"))
        elif not np.isnan(vTh):
            # check units of thermal velocity
            vTh = vTh.to(u.m / u.s)
    elif np.isnan(vTh) and units == "unitless":
        # assuming unitless temperature is in Kelvins
        vTh = (thermal_speed(T * u.K,
                             particle=particle,
                             method="most_probable")).si.value
    # getting distribution functions along each axis
    fx = Maxwellian_speed_1D(vx,
                             T,
                             particle=particle,
                             V_drift=Vx_drift,
                             vTh=vTh,
                             units=units)
    fy = Maxwellian_speed_1D(vy,
                             T,
                             particle=particle,
                             V_drift=Vy_drift,
                             vTh=vTh,
                             units=units)
    fz = Maxwellian_speed_1D(vz,
                             T,
                             particle=particle,
                             V_drift=Vz_drift,
                             vTh=vTh,
                             units=units)
    # multiplying probabilities in each axis to get 3D probability
    distFunc = fx * fy * fz
    if units == "units":
        return distFunc.to((u.s / u.m)**3)
    elif units == "unitless":
        return distFunc
def cutout_id_chem_map(yslice=slice(367,467), xslice=slice(114,214),
                       vrange=[51,60]*u.km/u.s, sourcename='e2',
                       filelist=glob.glob(paths.dpath('12m/cutouts/*e2e8*fits')),
                       source=None, radius=None,
                       chem_name='CH3OH',
                       shape=None, # check that shape matches slice
                      ):
    assert filelist

    maps = {}
    map_error = {}
    energies = {}
    degeneracies = {}
    frequencies = {}
    indices = {}

    # sanity check
    #shapes = [fits.getdata(fn).shape for fn in filelist]
    #assert len(set(shapes)) == 1

    for ii,fn in enumerate(ProgressBar(filelist)):
        if chem_name not in fn:
            log.debug("Skipping {0} because it doesn't have {1}".format(fn, chem_name))
            continue
        if 'temperature' in fn or 'column' in fn:
            continue

        if 'moment0' in fn:
            # there is a slight danger of off-by-one-pixel errors with the
            # cropping used here.  Ideally, we'd reproject...
            m0_full = fits.getdata(fn)
            #stddev = fits.getdata(fn.replace("moment0","madstd"))
            header = fits.getheader(fn)
            cutout = Cutout2D(m0_full, source, 2*radius, wcs=wcs.WCS(header))
            m0 = cutout.data
            #cutout_std = Cutout2D(stddev, source, 2*radius, wcs=wcs.WCS(header))
            #stddev = cutout_std.data
            try:
                beam = radio_beam.Beam.from_fits_header(header)
                jtok = beam.jtok(header['RESTFRQ']*u.Hz).value
            except:
                jtok = 222. # approximated 0.32x0.34 at 225 GHz

            m0 = m0 * jtok
            # stddev = error in a single channel... not very accurate
            #stddev = stddev * jtok
            print("stddev for {0}".format(fn)) # this debug statement prevents abort traps
            stddev = mad_std(m0_full[np.isfinite(m0_full)]) * jtok
            header = cutout.wcs.to_header()
        else:
            cube_ = SpectralCube.read(fn)
            if shape:
                # have to have same shapes, otherwise pixel slices don't make sense
                assert cube_.shape == shape
            cube = cube_[:,yslice,xslice]
            bm = cube.beams[0]
            #jtok = bm.jtok(cube.wcs.wcs.restfrq*u.Hz)
            cube = cube.to(u.K, bm.jtok_equiv(cube.wcs.wcs.restfrq*u.Hz))

            slab = cube.spectral_slab(*vrange)
            cube.beam_threshold = 1
            #contguess = cube.spectral_slab(0*u.km/u.s, 40*u.km/u.s).percentile(50, axis=0)
            #contguess = cube.spectral_slab(70*u.km/u.s, 100*u.km/u.s).percentile(50, axis=0)
            mask = (cube.spectral_axis<40*u.km/u.s) | (cube.spectral_axis > 75*u.km/u.s)
            contguess = cube.with_mask(mask[:,None,None]).percentile(30, axis=0)
            stddev = cube.with_mask(mask[:,None,None]).apply_numpy_function(mad_std, axis=0)
            slabsub = (slab-contguess)
            slabsub.beam_threshold = 0.15
            m0 = slabsub.moment0()
            header = m0.hdu.header

        label = linere.search(fn).groups()[0]
        frq = name_to_freq[label]

        closest_ind = np.argmin(np.abs(frqs - frq))
        closest_key = list(rt.keys())[closest_ind]
        closest_rt = rt[closest_key]
        upperstate = ch3oh.data['States'][closest_rt.UpperStateRef]
        upperen = u.Quantity(float(upperstate.StateEnergyValue),
                             unit=upperstate.StateEnergyUnit)

        maps[label] = m0
        map_error[label] = stddev
        energies[label] = upperen
        degeneracies[label] = int(upperstate.TotalStatisticalWeight)
        indices[label] = closest_ind
        frequencies[label] = frq

    # make sure the dict indices don't change order
    energy_to_key = {v:k for k,v in energies.items()}
    order = sorted(energy_to_key.keys())
    keys = [energy_to_key[k] for k in order]

    cube = np.empty((len(maps),)+maps[label].shape)
    ecube = np.empty_like(cube)
    xaxis = u.Quantity([energies[k] for k in keys])
    xaxis = xaxis.to(u.erg, u.spectral()).to(u.K, u.temperature_energy())
    for ii,key in enumerate(keys):
        # divide by degeneracy
        cube[ii,:,:] = maps[key]
        ecube[ii,:,:] = map_error[key]

    frequencies = u.Quantity([frequencies[k] for k in keys])
    indices = [indices[k] for k in keys]
    degeneracies = [degeneracies[k] for k in keys]

    assert xaxis.size == cube.shape[0]

    return xaxis,cube,ecube,maps,map_error,energies,frequencies,indices,degeneracies,header
Example #31
0
def kappa_velocity_3D(vx,
                      vy,
                      vz,
                      T,
                      kappa,
                      particle="e",
                      Vx_drift=0,
                      Vy_drift=0,
                      Vz_drift=0,
                      vTh=np.nan,
                      units="units"):
    r"""
    Return the probability of finding a particle with velocity components
    `v_x`, `v_y`, and `v_z`in m/s in a suprathermal plasma of temperature
    `T` and parameter 'kappa' which follows the 3D Kappa distribution function.
    This function assumes Cartesian coordinates.

    Parameters
    ----------
    vx: ~astropy.units.Quantity
        The velocity in x-direction units convertible to m/s.

    vy: ~astropy.units.Quantity
        The velocity in y-direction units convertible to m/s.

    vz: ~astropy.units.Quantity
        The velocity in z-direction units convertible to m/s.

    T: ~astropy.units.Quantity
        The temperature, preferably in Kelvin.

    kappa: ~astropy.units.Quantity
        The kappa parameter is a dimensionless number which sets the slope
        of the energy spectrum of suprathermal particles forming the tail
        of the Kappa velocity distribution function. Kappa must be greater
        than :math:`3/2`.

    particle: str, optional
        Representation of the particle species(e.g., 'p' for protons, 'D+'
        for deuterium, or 'He-4 +1' for :math:`He_4^{+1}` : singly ionized
        helium-4), which defaults to electrons.

    Vx_drift: ~astropy.units.Quantity, optional
        The drift velocity in x-direction units convertible to m/s.

    Vy_drift: ~astropy.units.Quantity, optional
        The drift velocity in y-direction units convertible to m/s.

    Vz_drift: ~astropy.units.Quantity, optional
        The drift velocity in z-direction units convertible to m/s.

    vTh: ~astropy.units.Quantity, optional
        Thermal velocity (most probable) in m/s. This is used for
        optimization purposes to avoid re-calculating `vTh`, for example
        when integrating over velocity-space.

    units: str, optional
        Selects whether to run function with units and unit checks (when
        equal to "units") or to run as unitless (when equal to "unitless").
        The unitless version is substantially faster for intensive
        computations.

    Returns
    -------
    f : ~astropy.units.Quantity
        probability in Velocity^-1, normalized so that:
        :math:`\iiint_{0}^{\infty} f(\vec{v}) d\vec{v} = 1`

    Raises
    ------
    TypeError
        The parameter arguments are not Quantities and
        cannot be converted into Quantities.

    ~astropy.units.UnitConversionError
        If the parameters is not in appropriate units.

    ValueError
        If the temperature is negative, or the particle mass or charge state
        cannot be found.

    Notes
    -----
    In three dimensions, the Kappa velocity distribution function describing
    the distribution of particles with speed :math:`v` in a plasma with
    temperature :math:`T` and suprathermal parameter :math:`\kappa` is given by:

    .. math::

       f = A_\kappa \left(1 + \frac{(\vec{v} -
       \vec{V_{drift}})^2}{\kappa v_Th,\kappa^2}\right)^{-(\kappa + 1)}

    where :math:`v_Th,\kappa` is the kappa thermal speed
    and :math:`A_\kappa = \frac{1}{2 \pi (\kappa v_Th,\kappa^2)^{3/2}}
    \frac{\Gamma(\kappa + 1)}{\Gamma(\kappa - 1/2) \Gamma(3/2)}` is the
    normalization constant.

    As :math:`\kappa` approaches infinity, the kappa distribution function
    converges to the Maxwellian distribution function.

    See also
    --------
    kappa_velocity_1D
    kappa_thermal_speed

    Example
    -------
    >>> from plasmapy.physics import kappa_velocity_3D
    >>> from astropy import units as u
    >>> v=1*u.m/u.s
    >>> kappa_velocity_3D(vx=v,
    ... vy=v,
    ... vz=v,
    ... T=30000*u.K,
    ... kappa=4,
    ... particle='e',
    ... Vx_drift=0*u.m/u.s,
    ... Vy_drift=0*u.m/u.s,
    ... Vz_drift=0*u.m/u.s)
    <Quantity 3.7833988e-19 s3 / m3>
    """
    # must have kappa > 3/2 for distribution function to be valid
    if kappa <= 3 / 2:
        raise ValueError(f"Must have kappa > 3/2, instead of {kappa}.")
    if units == "units":
        # unit checks and conversions
        # checking velocity units
        vx = vx.to(u.m / u.s)
        vy = vy.to(u.m / u.s)
        vz = vz.to(u.m / u.s)
        # catching case where drift velocities have default values, they
        # need to be assigned units
        if Vx_drift == 0:
            if not isinstance(Vx_drift, astropy.units.quantity.Quantity):
                Vx_drift = Vx_drift * u.m / u.s
        if Vy_drift == 0:
            if not isinstance(Vy_drift, astropy.units.quantity.Quantity):
                Vy_drift = Vy_drift * u.m / u.s
        if Vz_drift == 0:
            if not isinstance(Vz_drift, astropy.units.quantity.Quantity):
                Vz_drift = Vz_drift * u.m / u.s
        # checking units of drift velocities
        Vx_drift = Vx_drift.to(u.m / u.s)
        Vy_drift = Vy_drift.to(u.m / u.s)
        Vz_drift = Vz_drift.to(u.m / u.s)
        # convert temperature to Kelvins
        T = T.to(u.K, equivalencies=u.temperature_energy())
        if np.isnan(vTh):
            # get thermal velocity and thermal velocity squared
            vTh = kappa_thermal_speed(T, kappa, particle=particle)
        elif not np.isnan(vTh):
            # check units of thermal velocity
            vTh = vTh.to(u.m / u.s)
    elif np.isnan(vTh) and units == "unitless":
        # assuming unitless temperature is in Kelvins
        vTh = (kappa_thermal_speed(T * u.K, kappa, particle=particle)).si.value
    # getting square of thermal velocity
    vThSq = vTh**2
    # Get square of relative particle velocity
    vSq = ((vx - Vx_drift)**2 + (vy - Vy_drift)**2 + (vz - Vz_drift)**2)
    # calculating distribution function
    expTerm = (1 + vSq / (kappa * vThSq))**(-(kappa + 1))
    coeff1 = 1 / (2 * np.pi * (kappa * vThSq)**(3 / 2))
    coeff2 = gamma(kappa + 1) / (gamma(kappa - 1 / 2) * gamma(3 / 2))
    distFunc = coeff1 * coeff2 * expTerm
    if units == "units":
        return distFunc.to((u.s / u.m)**3)
    elif units == "unitless":
        return distFunc
Example #32
0
For example, plasmas at high (much larger than 1) Reynolds numbers are
highly turbulent, while turbulence is negligible at low Reynolds
numbers.
"""
__all__ = ['beta', 'quantum_theta']

from astropy import constants
from astropy import units as u
from plasmapy.formulary import (quantum, parameters)
from plasmapy.utils.decorators import validate_quantities


@validate_quantities(T={
    'can_be_negative': False,
    'equivalencies': u.temperature_energy()
},
                     n_e={'can_be_negative': False})
def quantum_theta(T: u.K, n_e: u.m**-3) -> u.dimensionless_unscaled:
    """
    Compares Fermi energy to thermal kinetic energy to check if quantum
    effects are important.

    Parameters
    ----------
    T : ~astropy.units.Quantity
        The temperature of the plasma.
    n_e : ~astropy.units.Quantity
          The electron number density of the plasma.

    Examples
Example #33
0
import numpy as np
import warnings

from astropy.constants.si import c, e, eps0, k_B

from plasmapy import particles
from plasmapy.formulary import frequencies, speeds
from plasmapy.particles import Particle
from plasmapy.utils.decorators import validate_quantities
from plasmapy.utils.exceptions import PlasmaPyFutureWarning

__all__ += __aliases__


@validate_quantities(
    T_e={"can_be_negative": False, "equivalencies": u.temperature_energy()},
    n_e={"can_be_negative": False},
)
def Debye_length(T_e: u.K, n_e: u.m**-3) -> u.m:
    r"""Calculate the characteristic decay length for electric fields,
     due to charge screening.

    **Aliases:** `lambdaD_`

    Parameters
    ----------
    T_e : `~astropy.units.Quantity`
        Electron temperature.

    n_e : `~astropy.units.Quantity`
        Electron number density.
Example #34
0
def cutout_id_chem_map(
    yslice=slice(367, 467),
    xslice=slice(114, 214),
    vrange=[51, 60] * u.km / u.s,
    sourcename='e2',
    filelist=glob.glob(paths.dpath('merge/cutouts/*natural*e2e8*fits')),
    linere=re.compile("W51_b6_7M_12M_natural.(.*).image.pbcor"),
    chem_name='HNCO',
):

    maps = {}
    energies = {}
    degeneracies = {}
    frequencies = {}
    indices = {}

    assert len(filelist) > 1

    for ii, fn in enumerate(ProgressBar(filelist)):
        if chem_name not in fn:
            log.debug("Skipping {0} because it doesn't have {1}".format(
                fn, chem_name))
            continue
        if 'temperature' in fn or 'column' in fn:
            continue

        cube = SpectralCube.read(fn)[:, yslice, xslice]
        print(yslice, xslice)
        print(cube)
        bm = cube.beams[0]
        #jtok = bm.jtok(cube.wcs.wcs.restfrq*u.Hz)
        cube = cube.to(u.K, bm.jtok_equiv(cube.wcs.wcs.restfrq * u.Hz))

        slab = cube.spectral_slab(*vrange)
        cube.beam_threshold = 1
        #contguess = cube.spectral_slab(0*u.km/u.s, 40*u.km/u.s).percentile(50, axis=0)
        #contguess = cube.spectral_slab(70*u.km/u.s, 100*u.km/u.s).percentile(50, axis=0)
        mask = (cube.spectral_axis < 40 * u.km / u.s) | (cube.spectral_axis >
                                                         75 * u.km / u.s)
        contguess = cube.with_mask(mask[:, None, None]).percentile(30, axis=0)
        slabsub = (slab - contguess)
        slabsub.beam_threshold = 0.15
        m0 = slabsub.moment0()

        label = linere.search(fn).groups()[0]
        frq = name_to_freq[label]

        closest_ind = np.argmin(np.abs(frqs - frq))
        closest_key = list(rt.keys())[closest_ind]
        closest_rt = rt[closest_key]
        upperstate = hnco.data['States'][closest_rt.UpperStateRef]
        upperen = u.Quantity(float(upperstate.StateEnergyValue),
                             unit=upperstate.StateEnergyUnit)

        maps[label] = m0
        energies[label] = upperen
        degeneracies[label] = int(upperstate.TotalStatisticalWeight)
        indices[label] = closest_ind
        frequencies[label] = frq

    # make sure the dict indices don't change order
    energy_to_key = {v: k for k, v in energies.items()}
    order = sorted(energy_to_key.keys())
    keys = [energy_to_key[k] for k in order]

    cube = np.empty((len(maps), ) + maps[label].shape)
    xaxis = u.Quantity([energies[k] for k in keys])
    xaxis = xaxis.to(u.erg, u.spectral()).to(u.K, u.temperature_energy())
    for ii, key in enumerate(keys):
        # divide by degeneracy
        cube[ii, :, :] = maps[key]

    frequencies = u.Quantity([frequencies[k] for k in keys])
    indices = [indices[k] for k in keys]
    degeneracies = [degeneracies[k] for k in keys]

    return xaxis, cube, maps, energies, frequencies, indices, degeneracies, m0.hdu.header
Example #35
0
"""Functions related to ionization states and the properties thereof."""

__all__ = ["ionization_balance", "Saha", "Z_bal_"]
__aliases__ = ["Z_bal_"]

import astropy.units as u

from astropy.constants import a0, k_B
from numpy import exp, log, pi, sqrt

from plasmapy.utils.decorators import validate_quantities


@validate_quantities(T_e={
    "can_be_negative": False,
    "equivalencies": u.temperature_energy()
})
def ionization_balance(n: u.m**-3, T_e: u.K) -> u.dimensionless_unscaled:
    r"""
    Return the average ionization state of ions in a plasma assuming that
    the numbers of ions in each state are equal.

    Z_bal is the estimate average ionization level of a plasma in thermal equilibrium
    that balances the number density of ions in two different ionization states.
    Z_bal is derived from the Saha equation with the assumptions that
    the atoms are of a single species,
    are either hydrogenic or completely ionized,
    and that there is a balance between ionization and recombination,
    meaning that the number of atoms in either state are equal.
    The Saha equation and therefore Z_bal are more accurate when the plasma
    is at a high density and temperature.
Example #36
0
def Maxwellian_speed_3D(vx,
                        vy,
                        vz,
                        T,
                        particle="e",
                        Vx_drift=0,
                        Vy_drift=0,
                        Vz_drift=0,
                        vTh=np.nan,
                        units="units"):
    r"""
    Return the probability of finding a particle with speed components
    `vx`, `vy`, and `vz`in m/s in an equilibrium plasma of temperature
    `T` which follows the 3D Maxwellian distribution function. This
    function assumes Cartesian coordinates.

    Parameters
    ----------
    vx: ~astropy.units.Quantity
        The speed in x-direction units convertible to m/s.

    vy: ~astropy.units.Quantity
        The speed in y-direction units convertible to m/s.

    vz: ~astropy.units.Quantity
        The speed in z-direction units convertible to m/s.

    T: ~astropy.units.Quantity
        The temperature, preferably in Kelvin.

    particle: str, optional
        Representation of the particle species(e.g., 'p' for protons, 'D+'
        for deuterium, or 'He-4 +1' for :math:`He_4^{+1}`
        (singly ionized helium-4), which defaults to electrons.

    Vx_drift: ~astropy.units.Quantity
        The drift speed in x-direction units convertible to m/s.

    Vy_drift: ~astropy.units.Quantity
        The drift speed in y-direction units convertible to m/s.

    Vz_drift: ~astropy.units.Quantity
        The drift speed in z-direction units convertible to m/s.

    vTh: ~astropy.units.Quantity, optional
        Thermal velocity (most probable) in m/s. This is used for
        optimization purposes to avoid re-calculating vTh, for example
        when integrating over velocity-space.

    units: str, optional
        Selects whether to run function with units and unit checks (when
        equal to "units") or to run as unitless (when equal to "unitless").
        The unitless version is substantially faster for intensive
        computations.

    Returns
    -------
    f : ~astropy.units.Quantity
        Probability in speed^-1, normalized so that:
        :math:`\iiint_{0}^{\infty} f(\vec{v}) d\vec{v} = 1`.

    Raises
    ------
    TypeError
        A parameter argument is not a `~astropy.units.Quantity` and
        cannot be converted into a `~astropy.units.Quantity`.

    ~astropy.units.UnitConversionError
        If the parameters is not in appropriate units.

    ValueError
        If the temperature is negative, or the particle mass or charge state
        cannot be found.

    Notes
    -----
    In one dimension, the Maxwellian speed distribution function describing
    the distribution of particles with speed :math:`v` in a plasma with
    temperature :math:`T` is given by:

    .. math::

       f = 4 \pi \vec{v}^2 (\pi * v_Th^2)^{-3/2} \exp(-(\vec{v} -
       \vec{V_{drift}})^2 / v_Th^2)

    where :math:`v_Th = \sqrt(2 k_B T / m)` is the thermal speed.

    See also
    --------
    Maxwellian_speed_1D

    Example
    -------
    >>> from plasmapy.physics import Maxwellian_speed_3D
    >>> from astropy import units as u
    >>> v=1*u.m/u.s
    >>> Maxwellian_speed_3D(vx=v,
    ... vy=v,
    ... vz=v,
    ... T=30000*u.K,
    ... particle='e',
    ... Vx_drift=0*u.m/u.s,
    ... Vy_drift=0*u.m/u.s,
    ... Vz_drift=0*u.m/u.s)
    <Quantity 1.76238544e-53 s3 / m3>

    """
    if units == "units":
        # unit checks and conversions
        # checking velocity units
        vx = vx.to(u.m / u.s)
        vy = vy.to(u.m / u.s)
        vz = vz.to(u.m / u.s)
        # catching case where drift velocities have default values, they
        # need to be assigned units
        if Vx_drift == 0:
            if not isinstance(Vx_drift, astropy.units.quantity.Quantity):
                Vx_drift = Vx_drift * u.m / u.s
        if Vy_drift == 0:
            if not isinstance(Vy_drift, astropy.units.quantity.Quantity):
                Vy_drift = Vy_drift * u.m / u.s
        if Vz_drift == 0:
            if not isinstance(Vz_drift, astropy.units.quantity.Quantity):
                Vz_drift = Vz_drift * u.m / u.s
        Vx_drift = Vx_drift.to(u.m / u.s)
        Vy_drift = Vy_drift.to(u.m / u.s)
        Vz_drift = Vz_drift.to(u.m / u.s)
        # convert temperature to Kelvins
        T = T.to(u.K, equivalencies=u.temperature_energy())
        if np.isnan(vTh):
            # get thermal velocity and thermal velocity squared
            vTh = (thermal_speed(T,
                                 particle=particle,
                                 method="most_probable"))
        elif not np.isnan(vTh):
            # check units of thermal velocity
            vTh = vTh.to(u.m / u.s)
    elif np.isnan(vTh) and units == "unitless":
        # assuming unitless temperature is in Kelvins
        vTh = (thermal_speed(T * u.K,
                             particle=particle,
                             method="most_probable")).si.value
    # getting distribution functions along each axis
    fx = Maxwellian_speed_1D(vx,
                             T,
                             particle=particle,
                             V_drift=Vx_drift,
                             vTh=vTh,
                             units=units)
    fy = Maxwellian_speed_1D(vy,
                             T,
                             particle=particle,
                             V_drift=Vy_drift,
                             vTh=vTh,
                             units=units)
    fz = Maxwellian_speed_1D(vz,
                             T,
                             particle=particle,
                             V_drift=Vz_drift,
                             vTh=vTh,
                             units=units)
    # multiplying probabilities in each axis to get 3D probability
    distFunc = fx * fy * fz
    if units == "units":
        return distFunc.to((u.s / u.m)**3)
    elif units == "unitless":
        return distFunc
Example #37
0
def thermal_speed(T, particle="e", method="most_probable"):
    r"""Returns the most probable speed for an particle within a Maxwellian
    distribution.

    Parameters
    ----------
    T : Quantity
        The particle temperature in either kelvin or energy per particle

    particle : string, optional
        Representation of the particle species (e.g., 'p' for protons, 'D+'
        for deuterium, or 'He-4 +1' for singly ionized helium-4),
        which defaults to electrons.  If no charge state information is
        provided, then the particles are assumed to be singly charged.

    Returns
    -------
    V : Quantity
        particle thermal speed

    Raises
    ------
    TypeError
        The particle temperature is not a Quantity

    UnitConversionError
        If the particle temperature is not in units of temperature or
        energy per particle

    ValueError
        The particle temperature is invalid or particle cannot be used to
        identify an isotope or particle

    UserWarning
        If the particle thermal speed exceeds 10% of the speed of light, or
        if units are not provided and SI units are assumed.

    Notes
    -----
    The particle thermal speed is given by:

    .. math::
    V_{th,i} = \sqrt{\frac{2 k_B T_i}{m_i}}

    This function yields the most probable speed within a distribution
    function.  However, the definition of thermal velocity varies by
    the square root of two depending on whether or not this velocity
    absorbs that factor in the expression for a Maxwellian
    distribution.  In particular, the expression given in the NRL
    Plasma Formulary [1] is a square root of two smaller than the
    result from this function.

    Examples
    --------
    >>> from astropy import units as u
    >>> thermal_speed(5*u.eV, 'p')
    <Quantity 30949.690182856546 m / s>
    >>> thermal_speed(1e6*u.K, particle='p')
    <Quantity 128486.55193256242 m / s>
    >>> thermal_speed(5*u.eV)
    <Quantity 1326205.1212395933 m / s>
    >>> thermal_speed(1e6*u.K)
    <Quantity 5505693.988425379 m / s>
    >>> thermal_speed(1e6*u.K, method="rms")
    <Quantity 6743070.475775486 m / s>
    >>> thermal_speed(1e6*u.K, method="mean_magnitude")
    <Quantity 19517177.023383822 m / s>

    """

    T = T.to(units.K, equivalencies=units.temperature_energy())

    try:
        m = ion_mass(particle)
    except Exception:
        raise ValueError(
            "Unable to find {} mass in thermal_speed".format(particle))

    # different methods, as per https://en.wikipedia.org/wiki/Thermal_velocity
    if method == "most_probable":
        V = (np.sqrt(2 * k_B * T / m)).to(units.m / units.s)
    elif method == "rms":
        V = (np.sqrt(3 * k_B * T / m)).to(units.m / units.s)
    elif method == "mean_magnitude":
        V = (np.sqrt(8 * k_B * T / (m / np.pi))).to(units.m / units.s)
    else:
        raise (ValueError(
            "Method {} not supported in thermal_speed".format(method)))

    return V
Example #38
0
def test_temperature_energy():
    x = 1000 * u.K
    y = (x * constants.k_B).to(u.keV)
    assert_allclose(x.to_value(u.keV, u.temperature_energy()), y.value)
    assert_allclose(y.to_value(u.K, u.temperature_energy()), x.value)
Example #39
0
def Debye_length(T_e, n_e):
    r"""Calculate the Debye length.

    Parameters
    ----------
    T_e: Quantity
        Electron temperature

    n_e: Quantity
        Electron number density

    Returns
    -------
    lambda_D: Quantity
        The Debye length in meters

    Raises
    ------
    TypeError
        If either argument is not a Quantity

    UnitConversionError
        If either argument is in incorrect units

    ValueError
        If either argument contains invalid values

    UserWarning
        If units are not provided and SI units are assumed

    Notes
    -----

    The Debye length is the exponential scale length for charge
    screening and is given by

    .. math::
    \lambda_D = \sqrt{\frac{\epsilon_0 k_b T_e}{n_e e^2}}

    for an electron plasma with nearly stationary ions.

    The electrical potential will drop by a factor of 1/e every Debye
    length.

    Plasmas will generally be quasineutral on length scales significantly
    larger than the Debye length.

    See also
    --------
    Debye_number

    Example
    -------
    >>> from astropy import units as u
    >>> Debye_length(5e6*u.K, 5e15*u.m**-3)
    <Quantity 0.002182255218625608 m>

    """

    T_e = T_e.to(units.K, equivalencies=units.temperature_energy())

    try:
        lambda_D = ((eps0 * k_B * T_e / (n_e * e**2))**0.5).to(units.m)
    except Exception:
        raise ValueError("Unable to find Debye length.")

    return lambda_D
Example #40
0
def _check_quantity(arg,
                    argname,
                    funcname,
                    units,
                    can_be_negative=True,
                    can_be_complex=False,
                    can_be_inf=True,
                    can_be_nan=True,
                    none_shall_pass=False):
    """
    Raise an exception if an object is not a `~astropy.units.Quantity`
    with correct units and valid numerical values.

    Parameters
    ----------
    arg : ~astropy.units.Quantity
        The object to be tested.

    argname : str
        The name of the argument to be printed in error messages.

    funcname : str
        The name of the original function to be printed in error messages.

    units : `~astropy.units.Unit` or list of `~astropy.unit.Unit`
        Acceptable units for `arg`.

    can_be_negative : bool, optional
        `True` if the `~astropy.units.Quantity` can be negative,
        `False` otherwise.  Defaults to `True`.

    can_be_complex : bool, optional
        `True` if the `~astropy.units.Quantity` can be a complex number,
        `False` otherwise.  Defaults to `False`.

    can_be_inf : bool, optional
        `True` if the `~astropy.units.Quantity` can contain infinite
        values, `False` otherwise.  Defaults to `True`.

    can_be_nan : bool, optional
        `True` if the `~astropy.units.Quantity` can contain NaN
        values, `False` otherwise.  Defaults to `True`.

    none_shall_pass : bool, optional
        `True` if the `~astropy.units.Quantity` can contain None
        values, `False` otherwise.  Defaults to `True`.

    Raises
    ------
    TypeError
        If the argument is not a `~astropy.units.Quantity` or units is
        not entirely units.

    ~astropy.units.UnitConversionError
        If the argument is not in acceptable units.

    ~astropy.units.UnitsError
        If after the assumption checks, the argument is still not in acceptable
        units.

    ValueError
        If the argument contains any `~numpy.nan` or other invalid
        values as determined by the keywords.

    Warns
    -----
    ~astropy.units.UnitsWarning
        If a `~astropy.units.Quantity` is not provided and unique units
        are provided, a `UnitsWarning` will be raised and the inputted
        units will be assumed.

    Examples
    --------
    >>> from astropy import units as u
    >>> import pytest
    >>> _check_quantity(4*u.T, 'B', 'f', u.T)
    <Quantity 4. T>
    >>> with pytest.warns(u.UnitsWarning, match="No units are specified"):
    ...     assert _check_quantity(4, 'B', 'f', u.T) == 4 * u.T

    """

    # TODO: Replace `funcname` with func.__name__?

    if not isinstance(units, list):
        units = [units]

    for unit in units:
        if not isinstance(unit, (u.Unit, u.CompositeUnit, u.IrreducibleUnit)):
            raise TypeError("The keyword 'units' to check_quantity must be "
                            "a unit or a list/tuple containing only units.")

    # Create a generic error message

    typeerror_message = (
        f"The argument {argname} to {funcname} should be a Quantity with ")

    if len(units) == 1:
        typeerror_message += f"the following units: {str(units[0])}"
    else:
        typeerror_message += "one of the following units: "
        for unit in units:
            typeerror_message += str(unit)
            if unit != units[-1]:
                typeerror_message += ", "
    if none_shall_pass:
        typeerror_message += "or None "

    if isinstance(arg, (u.Unit, u.CompositeUnit, u.IrreducibleUnit)):
        raise TypeError(typeerror_message)

    # Make sure arg is a quantity with correct units

    unit_casting_warning = dedent(
        f"""No units are specified for {argname} = {arg} in {funcname}. Assuming units of {str(units[0])}.
                To silence this warning, explicitly pass in an Astropy Quantity (from astropy.units)
                (see http://docs.astropy.org/en/stable/units/)""")

    # TODO include explicit note on how to pass in Astropy Quantity

    valueerror_message = (
        f"The argument {argname} to function {funcname} cannot contain")

    if arg is None and none_shall_pass:
        return arg
    elif arg is None:
        raise ValueError(f"{valueerror_message} Nones.")
    if not isinstance(arg, (u.Quantity)):
        if len(units) != 1:
            raise TypeError(typeerror_message)
        else:
            try:
                arg = arg * units[0]
            except (u.UnitsError, ValueError):
                raise TypeError(typeerror_message)
            else:
                warnings.warn(UnitsWarning(unit_casting_warning))
    if not isinstance(arg, u.Quantity):
        raise u.UnitsError(
            "{} is still not a Quantity after checks!".format(arg))

    in_acceptable_units = []

    for unit in units:
        try:
            arg.unit.to(unit, equivalencies=u.temperature_energy())
        except Exception:
            in_acceptable_units.append(False)
        else:
            in_acceptable_units.append(True)

    if not np.any(in_acceptable_units):
        raise u.UnitConversionError(typeerror_message)

    # Make sure that the quantity has valid numerical values
    if np.any(np.isnan(arg.value)) and not can_be_nan:
        raise ValueError(f"{valueerror_message} NaNs.")
    elif np.any(np.iscomplex(arg.value)) and not can_be_complex:
        raise ValueError(f"{valueerror_message} complex numbers.")
    elif not can_be_negative:
        # Allow NaNs through without raising a warning
        with np.errstate(invalid='ignore'):
            isneg = np.any(arg.value < 0)
        if isneg:
            raise ValueError(f"{valueerror_message} negative numbers.")
    elif not can_be_inf and np.any(np.isinf(arg.value)):
        raise ValueError(f"{valueerror_message} infs.")

    return arg
Example #41
0
def ion_sound_speed(T_e, T_i, gamma_e=1, gamma_i=3, ion='p+', z_mean=None):
    r"""
    Return the ion sound speed for an electron-ion plasma.

    Parameters
    ----------
    T_e : ~astropy.units.Quantity
        Electron temperature in units of temperature or energy per
        particle. If this is not given, then the electron temperature
        is assumed to be zero.

    T_i : ~astropy.units.Quantity
        Ion temperature in units of temperature or energy per
        particle.  If this is not given, then the ion temperature is
        assumed to be zero.

    gamma_e : float or int
        The adiabatic index for electrons, which defaults to 1.  This
        value assumes that the electrons are able to equalize their
        temperature rapidly enough that the electrons are effectively
        isothermal.

    gamma_i : float or int
        The adiabatic index for ions, which defaults to 3.  This value
        assumes that ion motion has only one degree of freedom, namely
        along magnetic field lines.

    ion : str, optional
        Representation of the ion species (e.g., `'p'` for protons,
        `'D+'` for deuterium, or 'He-4 +1' for singly ionized
        helium-4), which defaults to protons.  If no charge state
        information is provided, then the ions are assumed to be
        singly charged.

    z_mean : ~astropy.units.Quantity, optional
        The average ionization (arithmetic mean) for a plasma where the
        a macroscopic description is valid. If this quantity is not
        given then the atomic charge state (integer) of the ion
        is used. This is effectively an average ion sound speed for the
        plasma where multiple charge states are present.

    Returns
    -------
    V_S : ~astropy.units.Quantity
        The ion sound speed in units of meters per second.

    Raises
    ------
    TypeError
        If any of the arguments are not entered as keyword arguments
        or are of an incorrect type.

    ValueError
        If the ion mass, adiabatic index, or temperature are invalid.

    ~plasmapy.utils.PhysicsError
        If an adiabatic index is less than one.

    ~astropy.units.UnitConversionError
        If the temperature is in incorrect units.

    Warns
    -----
    RelativityWarning
        If the ion sound speed exceeds 5% of the speed of light.

    ~astropy.units.UnitsWarning
        If units are not provided, SI units are assumed.

    Notes
    -----
    The ion sound speed :math:`V_S` is approximately given by

    .. math::

        V_S = \sqrt{\frac{\gamma_e Z k_B T_e + \gamma_i k_B T_i}{m_i}}

    where :math:`\gamma_e` and :math:`\gamma_i` are the electron and
    ion adiabatic indices, :math:`k_B` is the Boltzmann constant,
    :math:`T_e` and :math:`T_i` are the electron and ion temperatures,
    :math:`Z` is the charge state of the ion, and :math:`m_i` is the
    ion mass.

    This function assumes that the product of the wavenumber and the
    Debye length is small. In this limit, the ion sound speed is not
    dispersive. In other words, it is frequency independent.

    When the electron temperature is much greater than the ion
    temperature, the ion sound velocity reduces to
    :math:`\sqrt{\gamma_e k_B T_e / m_i}`.  Ion acoustic waves can
    therefore occur even when the ion temperature is zero.

    Example
    -------
    >>> from astropy import units as u
    >>> ion_sound_speed(T_e=5e6*u.K, T_i=0*u.K, ion='p', gamma_e=1, gamma_i=3)
    <Quantity 203155.0764042 m / s>
    >>> ion_sound_speed(T_e=5e6*u.K, T_i=0*u.K)
    <Quantity 203155.0764042 m / s>
    >>> ion_sound_speed(T_e=500*u.eV, T_i=200*u.eV, ion='D+')
    <Quantity 229586.01860212 m / s>

    """

    m_i = atomic.ion_mass(ion)
    Z = grab_charge(ion, z_mean)

    for gamma, particles in zip([gamma_e, gamma_i], ["electrons", "ions"]):
        if not isinstance(gamma, (float, int)):
            raise TypeError(
                f"The adiabatic index gamma for {particles} must be "
                "a float or int")
        if gamma < 1:
            raise utils.PhysicsError(
                f"The adiabatic index for {particles} must be between "
                "one and infinity")

    T_i = T_i.to(u.K, equivalencies=u.temperature_energy())
    T_e = T_e.to(u.K, equivalencies=u.temperature_energy())

    try:
        V_S_squared = (gamma_e * Z * k_B * T_e + gamma_i * k_B * T_i) / m_i
        V_S = np.sqrt(V_S_squared).to(u.m / u.s)
    except Exception:
        raise ValueError("Unable to find ion sound speed.")

    return V_S
Example #42
0
def Maxwellian_speed_1D(v,
                        T,
                        particle="e",
                        V_drift=0,
                        vTh=np.nan,
                        units="units"):
    r"""
    Return the probability of finding a particle with speed `v` in m/s
    in an equilibrium plasma of temperature `T` which follows the
    Maxwellian distribution function.

    Parameters
    ----------
    v: ~astropy.units.Quantity
        The speed in units convertible to m/s.

    T: ~astropy.units.Quantity
        The temperature, preferably in Kelvin.

    particle: str, optional
        Representation of the particle species(e.g., `'p'` for protons, `'D+'`
        for deuterium, or `'He-4 +1'` for :math:`He_4^{+1}`
        (singly ionized helium-4), which defaults to electrons.

    V_drift: ~astropy.units.Quantity
        The drift speed in units convertible to m/s.

    vTh: ~astropy.units.Quantity, optional
        Thermal velocity (most probable) in m/s. This is used for
        optimization purposes to avoid re-calculating vTh, for example
        when integrating over velocity-space.

    units: str, optional
        Selects whether to run function with units and unit checks (when
        equal to "units") or to run as unitless (when equal to "unitless").
        The unitless version is substantially faster for intensive
        computations.

    Returns
    -------
    f : ~astropy.units.Quantity
        Probability in speed^-1, normalized so that
        :math:`\int_{0}^{\infty} f(v) dv = 1`.

    Raises
    ------
    TypeError
        The parameter arguments are not Quantities and
        cannot be converted into Quantities.

    ~astropy.units.UnitConversionError
        If the parameters is not in appropriate units.

    ValueError
        If the temperature is negative, or the particle mass or charge state
        cannot be found.

    Notes
    -----
    In one dimension, the Maxwellian speed distribution function describing
    the distribution of particles with speed v in a plasma with temperature T
    is given by:

    .. math::

       f(v) = 4 \pi v^2 (\pi * v_Th^2)^{-3/2} \exp(-(v - V_{drift})^2 / v_Th^2)

    where :math:`v_Th = \sqrt(2 k_B T / m)` is the thermal speed.

    Example
    -------
    >>> from plasmapy.physics import Maxwellian_speed_1D
    >>> from astropy import units as u
    >>> v=1*u.m/u.s
    >>> Maxwellian_speed_1D(v=v, T= 30000*u.K, particle='e',V_drift=0*u.m/u.s)
    <Quantity 2.60235754e-18 s / m>

    """
    if units == "units":
        # unit checks and conversions
        # checking velocity units
        v = v.to(u.m / u.s)
        # catching case where drift velocity has default value, and
        # needs to be assigned units
        if V_drift == 0:
            if not isinstance(V_drift, astropy.units.quantity.Quantity):
                V_drift = V_drift * u.m / u.s
        # checking drift velocity units
        V_drift = V_drift.to(u.m / u.s)
        # convert temperature to Kelvins
        T = T.to(u.K, equivalencies=u.temperature_energy())
        if np.isnan(vTh):
            # get thermal velocity and thermal velocity squared
            vTh = (thermal_speed(T,
                                 particle=particle,
                                 method="most_probable"))
        elif not np.isnan(vTh):
            # check units of thermal velocity
            vTh = vTh.to(u.m / u.s)
    elif np.isnan(vTh) and units == "unitless":
        # assuming unitless temperature is in Kelvins
        vTh = (thermal_speed(T * u.K,
                             particle=particle,
                             method="most_probable")).si.value
    # getting square of thermal speed
    vThSq = vTh ** 2
    # get square of relative particle speed
    vSq = (v - V_drift) ** 2
    # calculating distribution function
    coeff1 = (np.pi * vThSq) ** (-3 / 2)
    coeff2 = 4 * np.pi * vSq
    expTerm = np.exp(-vSq / vThSq)
    distFunc = coeff1 * coeff2 * expTerm
    if units == "units":
        return distFunc.to(u.s / u.m)
    elif units == "unitless":
        return distFunc
Example #43
0
def kappa_thermal_speed(T, kappa, particle="e-", method="most_probable"):
    r"""Return the most probable speed for a particle within a Kappa
    distribution.

    Parameters
    ----------
    T : ~astropy.units.Quantity
        The particle temperature in either kelvin or energy per particle

    kappa: float
        The kappa parameter is a dimensionless number which sets the slope
        of the energy spectrum of suprathermal particles forming the tail
        of the Kappa velocity distribution function. Kappa must be greater
        than 3/2.

    particle : str, optional
        Representation of the particle species (e.g., 'p' for protons, 'D+'
        for deuterium, or 'He-4 +1' for singly ionized helium-4),
        which defaults to electrons.  If no charge state information is
        provided, then the particles are assumed to be singly charged.

    method : str, optional
        Method to be used for calculating the thermal speed. Options are
        'most_probable' (default), 'rms', and 'mean_magnitude'.

    Returns
    -------
    V : ~astropy.units.Quantity
        Particle thermal speed

    Raises
    ------
    TypeError
        The particle temperature is not a ~astropy.units.Quantity.

    astropy.units.UnitConversionError
        If the particle temperature is not in units of temperature or
        energy per particle.

    ValueError
        The particle temperature is invalid or particle cannot be used to
        identify an isotope or particle.

    Warns
    -----
    RelativityWarning
        If the particle thermal speed exceeds 5% of the speed of light, or

    ~astropy.units.UnitsWarning
        If units are not provided, SI units are assumed.

    Notes
    -----
    The particle thermal speed is given by:

    .. math::
        V_{th,i} = \sqrt{(2 \kappa - 3)\frac{2 k_B T_i}{\kappa m_i}}

    For more discussion on the mean_magnitude calculation method, see [1]_.


    Examples
    --------
    >>> from astropy import units as u
    >>> kappa_thermal_speed(5*u.eV, 4, 'p') # defaults to most probable
    <Quantity 24467.87846359 m / s>
    >>> kappa_thermal_speed(5*u.eV, 4, 'p', 'rms')
    <Quantity 37905.47432261 m / s>
    >>> kappa_thermal_speed(5*u.eV, 4, 'p', 'mean_magnitude')
    <Quantity 34922.9856304 m / s>

    References
    ----------
    .. [1] PlasmaPy Issue #186, https://github.com/PlasmaPy/PlasmaPy/issues/186

    See Also
    --------
    plasmapy.physics.kappa_thermal_speed
    plasmapy.physics.kappa_velocity_1D
    """
    # Checking thermal units
    T = T.to(u.K, equivalencies=u.temperature_energy())
    if kappa <= 3 / 2:
        raise ValueError(f"Must have kappa > 3/2, instead of {kappa}, for "
                         "kappa distribution function to be valid.")
    # different methods, as per https://en.wikipedia.org/wiki/Thermal_velocity
    vTh = thermal_speed(T=T, particle=particle, method=method)

    if method == "most_probable":
        # thermal velocity of Kappa distribution function is just Maxwellian
        # thermal speed modulated by the following factor.
        # This is only true for "most probable" case. RMS and mean
        # magnitude velocities are same as Maxwellian.
        coeff = np.sqrt((kappa - 3 / 2) / kappa)
    else:
        coeff = 1

    return vTh * coeff
def cutout_id_chem_map(
    yslice=slice(367, 467),
    xslice=slice(114, 214),
    vrange=[51, 60] * u.km / u.s,
    sourcename="e2",
    filelist=glob.glob(paths.dpath("12m/cutouts/*e2e8*fits")),
    source=None,
    radius=None,
    molecular_database=ch3oh,
    radiative_transitions=rt,
    frqs=frqs,
    chem_name="CH3OH",
    shape=None,  # check that shape matches slice
):
    assert filelist

    maps = {}
    map_error = {}
    energies = {}
    degeneracies = {}
    frequencies = {}
    indices = {}

    # sanity check
    # shapes = [fits.getdata(fn).shape for fn in filelist]
    # assert len(set(shapes)) == 1

    for ii, fn in enumerate(ProgressBar(filelist)):
        if chem_name not in fn:
            log.debug("Skipping {0} because it doesn't have {1}".format(fn, chem_name))
            continue
        if "temperature" in fn or "column" in fn:
            continue

        if "moment0" in fn:
            # there is a slight danger of off-by-one-pixel errors with the
            # cropping used here.  Ideally, we'd reproject...
            m0_full = fits.getdata(fn)
            # stddev = fits.getdata(fn.replace("moment0","madstd"))
            header = fits.getheader(fn)
            cutout = Cutout2D(m0_full, source, 2 * radius, wcs=wcs.WCS(header))
            m0 = cutout.data
            # cutout_std = Cutout2D(stddev, source, 2*radius, wcs=wcs.WCS(header))
            # stddev = cutout_std.data
            try:
                beam = radio_beam.Beam.from_fits_header(header)
                jtok = beam.jtok(header["RESTFRQ"] * u.Hz).value
            except:
                jtok = 222.0  # approximated 0.32x0.34 at 225 GHz

            m0 = m0 * jtok
            # stddev = error in a single channel... not very accurate
            # stddev = stddev * jtok
            print("stddev for {0}".format(fn))  # this debug statement prevents abort traps
            stddev = mad_std(m0_full[np.isfinite(m0_full)]) * jtok
            header = cutout.wcs.to_header()
        else:
            cube_ = SpectralCube.read(fn)
            if shape:
                # have to have same shapes, otherwise pixel slices don't make sense
                assert cube_.shape == shape
            cube = cube_[:, yslice, xslice]
            bm = cube.beams[0]
            # jtok = bm.jtok(cube.wcs.wcs.restfrq*u.Hz)
            cube = cube.to(u.K, bm.jtok_equiv(cube.wcs.wcs.restfrq * u.Hz))

            slab = cube.spectral_slab(*vrange)
            cube.beam_threshold = 1
            # contguess = cube.spectral_slab(0*u.km/u.s, 40*u.km/u.s).percentile(50, axis=0)
            # contguess = cube.spectral_slab(70*u.km/u.s, 100*u.km/u.s).percentile(50, axis=0)
            mask = (cube.spectral_axis < 40 * u.km / u.s) | (cube.spectral_axis > 75 * u.km / u.s)
            contguess = cube.with_mask(mask[:, None, None]).percentile(30, axis=0)
            stddev = cube.with_mask(mask[:, None, None]).apply_numpy_function(mad_std, axis=0)
            slabsub = slab - contguess
            slabsub.beam_threshold = 0.15
            m0 = slabsub.moment0()
            header = m0.hdu.header

        label = linere.search(fn).groups()[0]
        frq = name_to_freq[label]

        closest_ind = np.argmin(np.abs(frqs - frq))
        closest_key = list(radiative_transitions.keys())[closest_ind]
        closest_rt = radiative_transitions[closest_key]
        upperstate = molecular_database.data["States"][closest_rt.UpperStateRef]
        upperen = u.Quantity(float(upperstate.StateEnergyValue), unit=upperstate.StateEnergyUnit)

        maps[label] = m0
        map_error[label] = stddev
        energies[label] = upperen
        degeneracies[label] = int(upperstate.TotalStatisticalWeight)
        indices[label] = closest_ind
        frequencies[label] = frq

    # make sure the dict indices don't change order
    energy_to_key = {v: k for k, v in energies.items()}
    order = sorted(energy_to_key.keys())
    keys = [energy_to_key[k] for k in order]

    cube = np.empty((len(maps),) + maps[label].shape)
    ecube = np.empty_like(cube)
    xaxis = u.Quantity([energies[k] for k in keys])
    xaxis = xaxis.to(u.erg, u.spectral()).to(u.K, u.temperature_energy())
    for ii, key in enumerate(keys):
        # divide by degeneracy
        cube[ii, :, :] = maps[key]
        ecube[ii, :, :] = map_error[key]

    frequencies = u.Quantity([frequencies[k] for k in keys])
    indices = [indices[k] for k in keys]
    degeneracies = [degeneracies[k] for k in keys]

    assert xaxis.size == cube.shape[0]

    return xaxis, cube, ecube, maps, map_error, energies, frequencies, indices, degeneracies, header
Example #45
0
def collision_rate_ion_ion(T_i,
                           n_i,
                           ion_particle,
                           coulomb_log=None,
                           V=None,
                           coulomb_log_method="classical"):
    r"""
    Momentum relaxation ion-ion collision rate

    From [3]_, equations (2.36) and (2.122)

    Considering a Maxwellian distribution of "test" ions colliding with
    a Maxwellian distribution of "field" ions.

    Note, it is assumed that electrons are present in such numbers as to
    establish quasineutrality, but the effects of the test ions colliding
    with them are not considered here.

    This result is an ion momentum relaxation rate, and is used in many
    classical transport expressions. It is equivalent to:
    * 1/tau_i from ref [1]_ eqn (1) pp. #,
    * 1/tau_i from ref [2]_ eqn (1) pp. #,
    * nu_i\i_S from ref [2]_ eqn (1) pp. #,

    Parameters
    ----------
    T_i : ~astropy.units.Quantity
        The electron temperature of the Maxwellian test ions

    n_i : ~astropy.units.Quantity
        The number density of the Maxwellian test ions

    ion_particle: str
        String signifying a particle type of the test and field ions,
        including charge state information. This function assumes the test
        and field ions are the same species.

    V : ~astropy.units.Quantity, optional
        The relative velocity between particles.  If not provided,
        thermal velocity is assumed: :math:`\mu V^2 \sim 2 k_B T`
        where `mu` is the reduced mass.

    coulomb_log : float or dimensionless ~astropy.units.Quantity, optional
        Option to specify a Coulomb logarithm of the electrons on the ions.
        If not specified, the Coulomb log will is calculated using the
        ~plasmapy.physics.transport.Coulomb_logarithm function.

    coulomb_log_method : string, optional
        Method used for Coulomb logarithm calculation (see that function
        for more documentation). Choose from "classical" or "GMS-1" to "GMS-6".

    References
    ----------
    .. [1] Braginskii

    .. [2] Formulary

    .. [3] Callen Chapter 2, http://homepages.cae.wisc.edu/~callen/chap2.pdf

    Examples
    --------
    >>> from astropy import units as u
    >>> collision_rate_ion_ion(0.1*u.eV, 1e6/u.m**3, 'p')
    <Quantity 2.97315582e-05 1 / s>
    >>> collision_rate_ion_ion(100*u.eV, 1e6/u.m**3, 'p')
    <Quantity 1.43713193e-09 1 / s>
    >>> collision_rate_ion_ion(100*u.eV, 1e20/u.m**3, 'p')
    <Quantity 66411.80316364 1 / s>
    >>> collision_rate_ion_ion(100*u.eV, 1e20/u.m**3, 'p', coulomb_log_method='GMS-1')
    <Quantity 66407.00859126 1 / s>
    >>> collision_rate_ion_ion(100*u.eV, 1e20/u.m**3, 'p', V = c/100)
    <Quantity 6.53577473 1 / s>
    >>> collision_rate_ion_ion(100*u.eV, 1e20/u.m**3, 'p', coulomb_log=20)
    <Quantity 95918.76240877 1 / s>

    """
    from plasmapy.physics.transport.collisions import Coulomb_logarithm
    T_i = T_i.to(u.K, equivalencies=u.temperature_energy())
    m_i = atomic.ion_mass(ion_particle)
    if V is not None:
        V = V
    else:
        # ion thermal velocity (most probable)
        V = np.sqrt(2 * k_B * T_i / m_i)
    if coulomb_log is not None:
        coulomb_log_val = coulomb_log
    else:
        particles = [ion_particle, ion_particle]
        coulomb_log_val = Coulomb_logarithm(T_i,
                                            n_i,
                                            particles,
                                            V,
                                            method=coulomb_log_method)
    Z_i = atomic.integer_charge(ion_particle)
    # this is the same as b_perp in collisions.py, using most probable thermal velocity for V
    # and using ion mass instead of reduced mass
    bperp = (Z_i * e)**2 / (4 * np.pi * eps0 * m_i * V**2)
    # collisional cross-section
    sigma = np.pi * (2 * bperp)**2
    # collisional frequency with Coulomb logarithm to correct for small angle collisions
    nu = n_i * sigma * V * coulomb_log_val
    # this coefficient is the constant that pops out when comparing this definition of
    # collisional frequency to the one in collisions.py
    coeff = np.sqrt(8 / np.pi) / 3
    # collisional frequency modified by the constant difference
    nu_i = coeff * nu
    return nu_i.to(1 / u.s)
Example #46
0
def test_temperature_energy():
    x = 1000 * u.K
    y = (x * constants.k_B).to(u.keV)
    assert_allclose(x.to_value(u.keV, u.temperature_energy()), y.value)
    assert_allclose(y.to_value(u.K, u.temperature_energy()), x.value)