Esempio n. 1
0
    def test_normal_vs_lite_values(self, kwargs, expected):
        """
        Test that `permittivity_1D_Maxwellian_lite` and
        `permittivity_1D_Maxwellian` calculate the same values.
        """

        wp = plasma_frequency(kwargs["n"], kwargs["particle"],
                              kwargs["z_mean"])
        vth = thermal_speed(kwargs["T"],
                            kwargs["particle"],
                            method="most_probable")
        kwargs["kWave"] = kwargs["omega"] / vth

        val = permittivity_1D_Maxwellian(**kwargs)
        val_lite = permittivity_1D_Maxwellian_lite(
            kwargs["omega"].value,
            kwargs["kWave"].to(u.rad / u.m).value,
            vth.value,
            wp.value,
        )

        assert (
            np.isclose(val, val_lite, rtol=1e-6, atol=0.0),
            "'permittivity_1D_Maxwellian' and 'permittivity_1D_Maxwellian_lite' "
            "do not agree.",
        )
Esempio n. 2
0
    def test_warns(self, args, kwargs, _warning, expected):
        """Test scenarios where `thermal_speed` issues warnings."""
        with pytest.warns(_warning):
            vth = thermal_speed(*args, **kwargs)
            assert vth.unit == u.m / u.s

            if expected is not None:
                assert vth == expected
Esempio n. 3
0
 def setup_class(self):
     """initializing parameters for tests"""
     self.T = 1.0 * u.eV
     self.particle = "H+"
     # get thermal velocity and thermal velocity squared
     self.vTh = thermal_speed(self.T,
                              particle=self.particle,
                              method="most_probable")
     self.v = 1e5 * u.m / u.s
     self.v_drift = 0 * u.m / u.s
     self.v_drift2 = 1e5 * u.m / u.s
     self.distFuncTrue = 1.8057567503860518e-25
Esempio n. 4
0
 def setup_class(self):
     """initializing parameters for tests"""
     self.T = 1.0 * u.eV
     self.particle = "H+"
     # get thermal velocity and thermal velocity squared
     self.vTh = thermal_speed(self.T,
                              particle=self.particle,
                              method="most_probable")
     self.v = 1e5 * u.m / u.s
     self.v_drift = 0 * u.m / u.s
     self.v_drift2 = 1e5 * u.m / u.s
     self.distFuncTrue = 1.72940389716217e-27
     self.distFuncDrift = 2 * (self.vTh**2 * np.pi)**(-1 / 2)
Esempio n. 5
0
    def test_known(self, kwargs, expected):
        """
        Tests permittivity_1D_Maxwellian for expected value.
        """

        vth = thermal_speed(kwargs["T"], kwargs["particle"], method="most_probable")
        kwargs["kWave"] = kwargs["omega"] / vth

        val = permittivity_1D_Maxwellian(**kwargs)
        assert (
            np.isclose(val, expected, rtol=1e-6, atol=0.0),
            f"Permittivity value should be {expected} and not {val}.",
        )
Esempio n. 6
0
    def test_correct_thermal_speed_used(self):
        """
        Test the correct version of thermal_speed is used when
        temperature is given.
        """
        B = 123 * u.G
        T = 1.2 * u.MK
        particle = "alpha"

        vperp = thermal_speed(T, particle=particle, method="most_probable", ndim=3)

        assert gyroradius(B, particle=particle, T=T) == gyroradius(
            B, particle=particle, Vperp=vperp
        )
Esempio n. 7
0
 def setup_class(self):
     """initializing parameters for tests"""
     self.T = 1.0 * u.eV
     self.particle = "H+"
     # get thermal velocity and thermal velocity squared
     self.vTh = thermal_speed(self.T,
                              particle=self.particle,
                              method="most_probable")
     self.vx = 1e5 * u.m / u.s
     self.vy = 1e5 * u.m / u.s
     self.vx_drift = 0 * u.m / u.s
     self.vy_drift = 0 * u.m / u.s
     self.vx_drift2 = 1e5 * u.m / u.s
     self.vy_drift2 = 1e5 * u.m / u.s
     self.distFuncTrue = 7.477094598799251e-55
Esempio n. 8
0
    def test_fail(self, kwargs, expected):
        """
        Tests if `test_known` would fail if we slightly adjusted the
        value comparison by some quantity close to numerical error.
        """
        vth = thermal_speed(kwargs["T"], kwargs["particle"], method="most_probable")
        kwargs["kWave"] = kwargs["omega"] / vth

        val = permittivity_1D_Maxwellian(**kwargs)

        expected += 1e-15
        assert (
            not np.isclose(val, expected, rtol=1e-16, atol=0.0),
            f"Permittivity value test gives {val} and should not be "
            f"equal to {expected}.",
        )
Esempio n. 9
0
 def setup_class(self):
     """initializing parameters for tests"""
     self.T_e = 30000 * u.K
     self.v = 1e5 * u.m / u.s
     self.v_drift = 1000000 * u.m / u.s
     self.v_drift2 = 0 * u.m / u.s
     self.v_drift3 = 1e5 * u.m / u.s
     self.start = -5000
     self.stop = -self.start
     self.dv = 10000 * u.m / u.s
     self.v_vect = np.arange(self.start, self.stop,
                             dtype="float64") * self.dv
     self.particle = "e"
     self.vTh = thermal_speed(self.T_e,
                              particle=self.particle,
                              method="most_probable")
     self.distFuncTrue = 5.851627151617136e-07
Esempio n. 10
0
    def test_normal_vs_lite_values(self, inputs):
        """
        Test that thermal_speed and thermal_speed_lite calculate the same values
        for the same inputs.
        """
        T_unitless = inputs["T"].to(u.K,
                                    equivalencies=u.temperature_energy()).value
        m_unitless = inputs["particle"].mass.value

        coeff = thermal_speed_coefficients(method=inputs["method"],
                                           ndim=inputs["ndim"])

        lite = thermal_speed_lite(T=T_unitless, mass=m_unitless, coeff=coeff)
        pylite = thermal_speed_lite.py_func(T=T_unitless,
                                            mass=m_unitless,
                                            coeff=coeff)
        assert pylite == lite

        normal = thermal_speed(**inputs)
        assert np.isclose(normal.value, lite)
Esempio n. 11
0
def gyroradius(
    B: u.T,
    particle: Particle,
    *,
    Vperp: u.m / u.s = np.nan * u.m / u.s,
    T_i: u.K = None,
    T: u.K = None,
) -> u.m:
    r"""Return the particle gyroradius.

    **Aliases:** `rc_`, `rhoc_`

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

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

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

    T : `~astropy.units.Quantity`, optional, keyword-only
        The particle temperature in units convertible to kelvin.

    T_i : `~astropy.units.Quantity`, optional, keyword-only
        The particle temperature in units convertible to kelvin.
        Note: Deprecated. Use ``T`` instead.

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

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

    `~astropy.units.UnitConversionError`
        The arguments do not have appropriate units.

    `ValueError`
        If any argument contains invalid values.

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

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

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

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

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

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

    Examples
    --------
    >>> from astropy import units as u
    >>> gyroradius(0.2*u.T, particle='p+', T=1e5*u.K)
    <Quantity 0.002120... m>
    >>> gyroradius(0.2*u.T, particle='p+', T=1e5*u.K)
    <Quantity 0.002120... m>
    >>> gyroradius(5*u.uG, particle='alpha', T=1*u.eV)
    <Quantity 288002.38... m>
    >>> gyroradius(400*u.G, particle='Fe+++', Vperp=1e7*u.m/u.s)
    <Quantity 48.23129... m>
    >>> gyroradius(B=0.01*u.T, particle='e-', T=1e6*u.K)
    <Quantity 0.003130... m>
    >>> gyroradius(0.01*u.T, 'e-', Vperp=1e6*u.m/u.s)
    <Quantity 0.000568... m>
    >>> gyroradius(0.2*u.T, 'e-', T=1e5*u.K)
    <Quantity 4.94949...e-05 m>
    >>> gyroradius(5*u.uG, 'e-', T=1*u.eV)
    <Quantity 6744.25... m>
    >>> gyroradius(400*u.G, 'e-', Vperp=1e7*u.m/u.s)
    <Quantity 0.001421... m>
    """

    # Backwards Compatibility and Deprecation check for keyword T_i
    if T_i is not None:
        warnings.warn(
            "Keyword T_i is deprecated, use T instead.",
            PlasmaPyFutureWarning,
        )
        if T is None:
            T = T_i
        else:
            raise ValueError(
                "Keywords T_i and T are both given.  T_i is deprecated, "
                "please use T only."
            )

    if T is None:
        T = np.nan * u.K

    isfinite_T = np.isfinite(T)
    isfinite_Vperp = np.isfinite(Vperp)

    # check 1: ensure either Vperp or T invalid, keeping in mind that
    # the underlying values of the astropy quantity may be numpy arrays
    if np.any(np.logical_and(isfinite_Vperp, isfinite_T)):
        raise ValueError(
            "Must give Vperp or T, but not both, as arguments to gyroradius"
        )

    # check 2: get Vperp as the thermal speed if is not already a valid input
    if np.isscalar(Vperp.value) and np.isscalar(
        T.value
    ):  # both T and Vperp are scalars
        # we know exactly one of them is nan from check 1
        if isfinite_T:
            # T is valid, so use it to determine Vperp
            Vperp = speeds.thermal_speed(T, particle=particle)
        # else: Vperp is already valid, do nothing
    elif np.isscalar(Vperp.value):  # only T is an array
        # this means either Vperp must be nan, or T must be an array of all nan,
        # or else we couldn't have gotten through check 1
        if isfinite_Vperp:
            # Vperp is valid, T is a vector that is all nan
            # uh...
            Vperp = np.repeat(Vperp, len(T))
        else:
            # normal case where Vperp is scalar nan and T is valid array
            Vperp = speeds.thermal_speed(T, particle=particle)
    elif np.isscalar(T.value):  # only Vperp is an array
        # this means either T must be nan, or V_perp must be an array of all nan,
        # or else we couldn't have gotten through check 1
        if isfinite_T:
            # T is valid, V_perp is an array of all nan
            # uh...
            Vperp = speeds.thermal_speed(np.repeat(T, len(Vperp)), particle=particle)
        # else: normal case where T is scalar nan and Vperp is already a valid
        # array so, do nothing
    else:  # both T and Vperp are arrays
        # we know all the elementwise combinations have one nan and one finite,
        # due to check 1 use the valid Vperps, and replace the others with those
        # calculated from T
        Vperp = Vperp.copy()  # avoid changing Vperp's value outside function
        Vperp[isfinite_T] = speeds.thermal_speed(T[isfinite_T], particle=particle)

    omega_ci = frequencies.gyrofrequency(B, particle)

    return np.abs(Vperp) / omega_ci
Esempio n. 12
0
def Maxwellian_speed_3D(v, T, particle="e", v_drift=0, vTh=np.nan, units="units"):
    r"""
    Probability distribution function of speed for a Maxwellian
    distribution in 3D.

    Return the probability density function for 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
    ----------
    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 density in speed\ :sup:`-1`\ , normalized so that:
        :math:`\iiint_0^∞ 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 3D, 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 π v^{2} (π v_{Th}^2)^{-3/2} \exp(-v^{2} / v_{Th}^2)

    where :math:`v_{Th} = \sqrt{2 k_B T / m}` is the thermal speed.

    See Also
    --------
    Maxwellian_speed_1D

    Examples
    --------
    >>> from astropy import units as u
    >>> v=1 * u.m / u.s
    >>> Maxwellian_speed_3D(v=v, T=30000*u.K, particle='e', v_drift=0 * u.m / u.s)
    <Quantity 2.60235...e-18 s / m>

    """
    if v_drift != 0:
        raise NotImplementedError("Non-zero drift speed is work in progress.")
    if units == "units":
        # unit checks and conversions
        # checking velocity units
        v = v.to_value(SPEED_UNITS)
        # Catching case where drift velocity has default value, and
        # needs to be assigned units
        v_drift = _v_drift_conversion(v_drift)
        # convert temperature to kelvin
        T = T.to_value(u.K, equivalencies=u.temperature_energy())
        if not np.isnan(vTh):
            # check units of thermal velocity
            vTh = vTh.to_value(SPEED_UNITS)

    if np.isnan(vTh):
        # get thermal velocity and thermal velocity squared
        vTh = thermal_speed(
            T << u.K, particle=particle, method="most_probable"
        ).to_value(SPEED_UNITS)

    # 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 << SPEED_DISTRIBUTION_UNITS_1D
    elif units == "unitless":
        return distFunc
Esempio n. 13
0
def Maxwellian_speed_1D(v, T, particle="e", v_drift=0, vTh=np.nan, units="units"):
    r"""
    Probability distribution function of speed for a Maxwellian distribution
    in 1D.

    Return the probability density function for 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 density in speed\ :sup:`-1`\ , normalized so that
        :math:`\int_{0}^∞ 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 :math:`v` in a plasma with
    temperature :math:`T` is given by:

    .. math::

       f(v) = 2 \frac{1}{(π v_{Th}^2)^{1/2}} \exp(-(v - V_{drift})^2 / v_{Th}^2 )

    where :math:`v_{Th} = \sqrt{2 k_B T / m}` is the thermal speed.

    Examples
    --------
    >>> 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 1.1832...e-06 s / m>

    """
    if units == "units":
        # unit checks and conversions
        # checking velocity units
        v = v.to_value(SPEED_UNITS)
        # Catching case where drift velocities have default values, they
        # need to be assigned units
        v_drift = _v_drift_conversion(v_drift)
        # convert temperature to kelvin
        T = T.to_value(u.K, equivalencies=u.temperature_energy())
        if not np.isnan(vTh):
            # check units of thermal velocity
            vTh = vTh.to_value(SPEED_UNITS)

    if np.isnan(vTh):
        # get thermal velocity and thermal velocity squared
        vTh = thermal_speed(
            T << u.K, particle=particle, method="most_probable"
        ).to_value(SPEED_UNITS)

    # Get thermal velocity squared
    vThSq = vTh**2
    # Get square of relative particle velocity
    vSq = (v - v_drift) ** 2
    # calculating distribution function
    coeff = 2 * (vThSq * np.pi) ** (-1 / 2)
    expTerm = np.exp(-vSq / vThSq)
    distFunc = coeff * expTerm
    if units == "units":
        return distFunc << SPEED_DISTRIBUTION_UNITS_1D
    elif units == "unitless":
        return distFunc
Esempio n. 14
0
def Maxwellian_velocity_3D(
    vx,
    vy,
    vz,
    T,
    particle="e",
    vx_drift=0,
    vy_drift=0,
    vz_drift=0,
    vTh=np.nan,
    units="units",
):
    r"""
    Probability distribution function of velocity for a Maxwellian
    distribution in 3D.

    Return the probability density function for finding a particle with
    velocity 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 velocity in x-direction in 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.

    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.

    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 density in Velocity^-1, normalized so that
        :math:`\iiint_{0}^∞ 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 3D, 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 = (\pi v_{Th}^2)^{-3/2} \exp \left [-(\vec{v} -
        \vec{V}_{drift})^2 / v_{Th}^2 \right ]

    where :math:`v_{Th} = \sqrt{2 k_B T / m}` is the thermal speed.

    See Also
    --------
    Maxwellian_1D

    Examples
    --------
    >>> from astropy import units as u
    >>> v=1 * u.m / u.s
    >>> Maxwellian_velocity_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 2.0708...e-19 s3 / m3>


    """
    if units == "units":
        # unit checks and conversions
        # checking velocity units
        vx = vx.to_value(SPEED_UNITS)
        vy = vy.to_value(SPEED_UNITS)
        vz = vz.to_value(SPEED_UNITS)
        # catching case where drift velocities have default values, they
        # need to be assigned units
        vx_drift = _v_drift_conversion(vx_drift)
        vy_drift = _v_drift_conversion(vy_drift)
        vz_drift = _v_drift_conversion(vz_drift)
        # convert temperature to kelvin
        T = T.to_value(u.K, equivalencies=u.temperature_energy())
        if not np.isnan(vTh):
            # check units of thermal velocity
            vTh = vTh.to_value(SPEED_UNITS)

    if np.isnan(vTh):
        # get thermal velocity and thermal velocity squared
        vTh = thermal_speed(
            T << u.K, particle=particle, method="most_probable"
        ).to_value(SPEED_UNITS)

    # accounting for thermal velocity in 3D
    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
    coeff = (vThSq * np.pi) ** (-3 / 2)
    expTerm = np.exp(-vSq / vThSq)
    distFunc = coeff * expTerm
    if units == "units":
        return distFunc << SPEED_DISTRIBUTION_UNITS_3D
    elif units == "unitless":
        return distFunc
Esempio n. 15
0
def permittivity_1D_Maxwellian(
    omega: u.rad / u.s,
    kWave: u.rad / u.m,
    T: u.K,
    n: u.m**-3,
    particle,
    z_mean: u.dimensionless_unscaled = None,
) -> u.dimensionless_unscaled:
    r"""
    Compute the classical dielectric permittivity for a 1D Maxwellian
    plasma.

    This function can calculate both the ion and electron
    permittivities.  No additional effects are considered (e.g.
    magnetic fields, relativistic effects, strongly coupled regime, etc.).

    Parameters
    ----------
    omega : `~astropy.units.Quantity`
        The frequency, in rad/s, of the electromagnetic wave propagating
        through the plasma.

    kWave : `~astropy.units.Quantity`
        The corresponding wavenumber, in rad/m, of the electromagnetic
        wave propagating through the plasma.

    T : `~astropy.units.Quantity`
        The plasma temperature — this can be either the electron or the
        ion temperature, but should be consistent with density and
        particle.

    n : `~astropy.units.Quantity`
        The plasma density — this can be either the electron or the ion
        density, but should be consistent with temperature and particle.

    particle : `str`
        The plasma particle species.

    z_mean : `~numbers.Real`
        The average ionization of the plasma. This is only required for
        calculating the ion permittivity.

    Returns
    -------
    chi : `~astropy.units.Quantity`
        The ion or the electron dielectric permittivity of the plasma.
        This is a dimensionless quantity.

    Notes
    -----
    The dielectric permittivities for a Maxwellian plasma are described
    by the following equations (see p. 106 of :cite:t:`froula:2011`):

    .. math::
        χ_e(k, ω) = - \frac{α_e^2}{2} Z'(x_e)

        χ_i(k, ω) = - \frac{α_i^2}{2}\frac{Z}{} Z'(x_i)

        α = \frac{ω_p}{k v_{Th}}

        x = \frac{ω}{k v_{Th}}

    :math:`χ_e` and :math:`χ_i` are the electron and ion permittivities,
    respectively. :math:`Z'` is the derivative of the plasma dispersion
    function. :math:`α` is the scattering parameter which delineates
    the difference between the collective and non-collective Thomson
    scattering regimes. :math:`x` is the dimensionless phase velocity
    of the electromagnetic wave propagating through the plasma.

    Examples
    --------
    >>> from astropy import units as u
    >>> from numpy import pi
    >>> from plasmapy.formulary import thermal_speed
    >>> T = 30 * 11600 * u.K
    >>> n = 1e18 * u.cm**-3
    >>> particle = 'Ne'
    >>> z_mean = 8 * u.dimensionless_unscaled
    >>> vth = thermal_speed(T, particle, method="most_probable")
    >>> omega = 5.635e14 * 2 * pi * u.rad / u.s
    >>> k_wave = omega / vth
    >>> permittivity_1D_Maxwellian(omega, k_wave, T, n, particle, z_mean)
    <Quantity -6.72809...e-08+5.76037...e-07j>

    For user convenience
    `~plasmapy.formulary.dielectric.permittivity_1D_Maxwellian_lite`
    is bound to this function and can be used as follows:

    >>> from plasmapy.formulary import plasma_frequency
    >>> wp = plasma_frequency(n, particle, z_mean=z_mean)
    >>> permittivity_1D_Maxwellian.lite(
    ...     omega.value, k_wave.value, vth=vth.value, wp=wp.value
    ... )
    (-6.72809...e-08+5.76037...e-07j)
    """
    vth = thermal_speed(T=T, particle=particle, method="most_probable").value
    wp = plasma_frequency(n=n, particle=particle, z_mean=z_mean).value

    chi = permittivity_1D_Maxwellian_lite(
        omega.value,
        kWave.value,
        vth,
        wp,
    )
    return chi * u.dimensionless_unscaled
Esempio n. 16
0
class TestThermalSpeed:
    """
    Test class for functionality of
    `plasmapy.formulary.speeds.thermal_speed`, which include...

    - Scenarios for raised exceptions
    - Scenarios for issued warnings
    - Basic behavior of `thermal_speed`
    - Proper binding of Lite-Function functionality

    Note: Testing of `thermal_speed_coefficients` and
    `thermal_speed_lite` are done in separate test classes.
    """
    @pytest.mark.parametrize(
        "bound_name, bound_attr",
        [
            ("lite", thermal_speed_lite),
            ("coefficients", thermal_speed_coefficients),
        ],
    )
    def test_lite_function_binding(self, bound_name, bound_attr):
        """Test expected attributes are bound correctly."""
        assert hasattr(thermal_speed, bound_name)
        assert getattr(thermal_speed, bound_name) is bound_attr

    def test_lite_function_marking(self):
        """
        Test thermal_speed is marked as having a Lite-Function.
        """
        assert hasattr(thermal_speed, "__bound_lite_func__")
        assert isinstance(thermal_speed.__bound_lite_func__, dict)

        for bound_name, bound_origin in thermal_speed.__bound_lite_func__.items(
        ):
            assert hasattr(thermal_speed, bound_name)

            attr = getattr(thermal_speed, bound_name)
            origin = f"{attr.__module__}.{attr.__name__}"
            assert origin == bound_origin

    @pytest.mark.parametrize(
        "args, kwargs, expected",
        [
            # Parameters that should return the value of the thermal
            # speed coefficient.
            #  - note the mass kwarg is overriding particle="e-"
            (
                ((1 / k_B.value) * u.K, "e-"),
                {
                    "mass": 1 * u.kg,
                    "ndim": 1,
                    "method": "most_probable"
                },
                0,
            ),
            (
                ((1 / k_B.value) * u.K, "e-"),
                {
                    "mass": 1 * u.kg,
                    "ndim": 2,
                    "method": "most_probable"
                },
                1.0,
            ),
            (
                ((1 / k_B.value) * u.K, "e-"),
                {
                    "mass": 1 * u.kg,
                    "ndim": 3,
                    "method": "most_probable"
                },
                np.sqrt(2),
            ),  # same as default kwarg values
            (((1 / k_B.value) * u.K, "e-"), {
                "mass": 1 * u.kg
            }, np.sqrt(2)),
            (
                ((1 / k_B.value) * u.K, "e-"),
                {
                    "mass": 1 * u.kg,
                    "ndim": 1,
                    "method": "rms"
                },
                1,
            ),
            (
                ((1 / k_B.value) * u.K, "e-"),
                {
                    "mass": 1 * u.kg,
                    "ndim": 2,
                    "method": "rms"
                },
                np.sqrt(2),
            ),
            (
                ((1 / k_B.value) * u.K, "e-"),
                {
                    "mass": 1 * u.kg,
                    "ndim": 3,
                    "method": "rms"
                },
                np.sqrt(3),
            ),
            (
                ((1 / k_B.value) * u.K, "e-"),
                {
                    "mass": 1 * u.kg,
                    "ndim": 1,
                    "method": "mean_magnitude"
                },
                np.sqrt(2 / np.pi),
            ),
            (
                ((1 / k_B.value) * u.K, "e-"),
                {
                    "mass": 1 * u.kg,
                    "ndim": 2,
                    "method": "mean_magnitude"
                },
                np.sqrt(np.pi / 2),
            ),
            (
                ((1 / k_B.value) * u.K, "e-"),
                {
                    "mass": 1 * u.kg,
                    "ndim": 3,
                    "method": "mean_magnitude"
                },
                np.sqrt(8 / np.pi),
            ),
            (
                ((1 / k_B.value) * u.K, "e-"),
                {
                    "mass": 1 * u.kg,
                    "ndim": 1,
                    "method": "nrl"
                },
                1.0,
            ),
            (
                ((1 / k_B.value) * u.K, "e-"),
                {
                    "mass": 1 * u.kg,
                    "ndim": 2,
                    "method": "nrl"
                },
                1.0,
            ),
            (
                ((1 / k_B.value) * u.K, "e-"),
                {
                    "mass": 1 * u.kg,
                    "ndim": 3,
                    "method": "nrl"
                },
                1.0,
            ),
            #
            # Select values for proton and electron thermal speeds.
            ((1 * u.MK, "e-"), {}, 5505694.743141063),
            ((1 * u.MK, "p"), {}, 128486.56960876315),
            ((1e6 * u.K, "e-"), {
                "method": "rms",
                "ndim": 1
            }, 3893114.2008620175),
            (
                (1e6 * u.K, "e-"),
                {
                    "method": "mean_magnitude",
                    "ndim": 1
                },
                3106255.714310189,
            ),
            (
                (1e6 * u.K, "e-"),
                {
                    "method": "most_probable",
                    "ndim": 2
                },
                3893114.2008620175,
            ),
            ((1e6 * u.K, "e-"), {
                "method": "rms",
                "ndim": 2
            }, 5505694.902726359),
            (
                (1e6 * u.K, "e-"),
                {
                    "method": "mean_magnitude",
                    "ndim": 2
                },
                4879295.066124102,
            ),
            (
                (1e6 * u.K, "e-"),
                {
                    "method": "most_probable",
                    "ndim": 3
                },
                5505694.902726359,
            ),
            ((1e6 * u.K, "e-"), {
                "method": "rms",
                "ndim": 3
            }, 6743071.595560921),
            (
                (1e6 * u.K, "e-"),
                {
                    "method": "mean_magnitude",
                    "ndim": 3
                },
                6212511.428620378,
            ),
            #
            # Cases that assume Z=1
            ((1e6 * u.K, "p"), {}, thermal_speed(1e6 * u.K, "H-1+").value),
            ((5 * u.eV, "e+"), {}, thermal_speed(5 * u.eV, "e-").value),
            (
                (1 * u.eV, "He"),
                {},
                thermal_speed(1 * u.eV, "He+", mass=Particle("He").mass).value,
            ),
        ],
    )
    def test_values(self, args, kwargs, expected):
        """Test scenarios with known calculated values."""
        vth = thermal_speed(*args, **kwargs)
        assert np.allclose(vth.value, expected)
        assert vth.unit == u.m / u.s

    @pytest.mark.parametrize(
        "args, kwargs, _error",
        [
            ((5 * u.m, "e-"), {}, u.UnitTypeError),
            ((5 * u.m, "He+"), {}, u.UnitTypeError),
            ((-5 * u.K, "e-"), {}, ValueError),
            ((-5 * u.eV, "e-"), {}, ValueError),
            ((5e19 * u.K, "e-"), {}, RelativityError),
            ((1e6 * u.K, ), {
                "particle": "not a valid particle"
            }, InvalidParticleError),
            ((1e6 * u.K, "e-"), {
                "method": "not valid"
            }, ValueError),
            ((1e6 * u.K, "e-"), {
                "ndim": 4
            }, ValueError),
        ],
    )
    def test_raises(self, args, kwargs, _error):
        """Test scenarios that cause an `Exception` to be raised."""
        with pytest.raises(_error):
            thermal_speed(*args, **kwargs)

    @pytest.mark.parametrize(
        "args, kwargs, _warning, expected",
        [
            ((), {
                "T": 1e9 * u.K,
                "particle": "e-"
            }, RelativityWarning, None),
            (
                (1e5, ),
                {
                    "particle": "e-"
                },
                u.UnitsWarning,
                thermal_speed(1e5 * u.K, "e-"),
            ),
            ((1e11 * u.K, "p"), {}, RelativityWarning, None),
            ((1e6, "p"), {}, u.UnitsWarning, thermal_speed(1e6 * u.K, "p")),
        ],
    )
    def test_warns(self, args, kwargs, _warning, expected):
        """Test scenarios where `thermal_speed` issues warnings."""
        with pytest.warns(_warning):
            vth = thermal_speed(*args, **kwargs)
            assert vth.unit == u.m / u.s

            if expected is not None:
                assert vth == expected

    def test_electron_vs_proton(self):
        """
        Ensure the electron thermal speed is larger that the proton
        thermal speed for the same parameters.
        """
        assert thermal_speed(1e6 * u.K, "e-") > thermal_speed(1e6 * u.K, "p")

    def test_can_handle_numpy_arrays(self):
        assert_can_handle_nparray(thermal_speed)
Esempio n. 17
0
 def test_electron_vs_proton(self):
     """
     Ensure the electron thermal speed is larger that the proton
     thermal speed for the same parameters.
     """
     assert thermal_speed(1e6 * u.K, "e-") > thermal_speed(1e6 * u.K, "p")
Esempio n. 18
0
 def test_raises(self, args, kwargs, _error):
     """Test scenarios that cause an `Exception` to be raised."""
     with pytest.raises(_error):
         thermal_speed(*args, **kwargs)
Esempio n. 19
0
 def test_values(self, args, kwargs, expected):
     """Test scenarios with known calculated values."""
     vth = thermal_speed(*args, **kwargs)
     assert np.allclose(vth.value, expected)
     assert vth.unit == u.m / u.s
Esempio n. 20
0
def spectral_density(
    wavelengths: u.nm,
    probe_wavelength: u.nm,
    n: u.m**-3,
    Te: u.K,
    Ti: u.K,
    efract: np.ndarray = None,
    ifract: np.ndarray = None,
    ion_species: Union[str, List[str], Particle, List[Particle]] = "H+",
    electron_vel: u.m / u.s = None,
    ion_vel: u.m / u.s = None,
    probe_vec=np.array([1, 0, 0]),
    scatter_vec=np.array([0, 1, 0]),
) -> Tuple[Union[np.floating, np.ndarray], np.ndarray]:
    r"""
    Calculate the spectral density function for Thomson scattering of a
    probe laser beam by a multi-species Maxwellian plasma.

    This function calculates the spectral density function for Thomson
    scattering of a probe laser beam by a plasma consisting of one or more ion
    species and a one or more thermal electron populations (the entire plasma
    is assumed to be quasi-neutral)

    .. math::
        S(k,ω) = \sum_e \frac{2π}{k}
        \bigg |1 - \frac{χ_e}{ε} \bigg |^2
        f_{e0,e} \bigg (\frac{ω}{k} \bigg ) +
        \sum_i \frac{2π Z_i}{k}
        \bigg |\frac{χ_e}{ε} \bigg |^2 f_{i0,i}
        \bigg ( \frac{ω}{k} \bigg )

    where :math:`χ_e` is the electron component susceptibility of the
    plasma and :math:`ε = 1 + \sum_e χ_e + \sum_i χ_i` is the total
    plasma dielectric  function (with :math:`χ_i` being the ion component
    of the susceptibility), :math:`Z_i` is the charge of each ion, :math:`k`
    is the scattering wavenumber, :math:`ω` is the scattering frequency,
    and :math:`f_{e0,e}` and :math:`f_{i0,i}` are the electron and ion velocity
    distribution functions respectively. In this function the electron and ion
    velocity distribution functions are assumed to be Maxwellian, making this
    function equivalent to Eq. 3.4.6 in :cite:p:`sheffield:2011`\ .

    Parameters
    ----------

    wavelengths : `~astropy.units.Quantity`
        Array of wavelengths over which the spectral density function
        will be calculated. (convertible to nm)

    probe_wavelength : `~astropy.units.Quantity`
        Wavelength of the probe laser. (convertible to nm)

    n : `~astropy.units.Quantity`
        Mean (0th order) density of all plasma components combined.
        (convertible to cm\ :sup:`-3`\ .)

    Te : `~astropy.units.Quantity`, shape (Ne, )
        Temperature of each electron component. Shape (Ne, ) must be equal to the
        number of electron components Ne. (in K or convertible to eV)

    Ti : `~astropy.units.Quantity`, shape (Ni, )
        Temperature of each ion component. Shape (Ni, ) must be equal to the
        number of ion components Ni. (in K or convertible to eV)

    efract : array_like, shape (Ne, ), optional
        An array-like object where each element represents the fraction (or ratio)
        of the electron component number density to the total electron number density.
        Must sum to 1.0. Default is a single electron component.

    ifract : array_like, shape (Ni, ), optional
        An array-like object where each element represents the fraction (or ratio)
        of the ion component number density to the total ion number density.
        Must sum to 1.0. Default is a single ion species.

    ion_species : `str` or `~plasmapy.particles.particle_class.Particle`, shape (Ni, ), optional
        A list or single instance of `~plasmapy.particles.Particle`, or
        strings convertible to `~plasmapy.particles.particle_class.Particle`.
        Default is ``'H+'`` corresponding to a single species of hydrogen ions.

    electron_vel : `~astropy.units.Quantity`, shape (Ne, 3), optional
        Velocity of each electron component in the rest frame. (convertible to m/s)
        Defaults to a stationary plasma [0, 0, 0] m/s.

    ion_vel : `~astropy.units.Quantity`, shape (Ni, 3), optional
        Velocity vectors for each electron population in the rest frame
        (convertible to m/s) Defaults zero drift
        for all specified ion species.

    probe_vec : float `~numpy.ndarray`, shape (3, )
        Unit vector in the direction of the probe laser. Defaults to
        [1, 0, 0].

    scatter_vec : float `~numpy.ndarray`, shape (3, )
        Unit vector pointing from the scattering volume to the detector.
        Defaults to [0, 1, 0] which, along with the default `probe_vec`,
        corresponds to a 90° scattering angle geometry.

    Returns
    -------
    alpha : `float`
        Mean scattering parameter, where ``alpha`` > 1 corresponds to collective
        scattering and ``alpha`` < 1 indicates non-collective scattering. The
        scattering parameter is calculated based on the total plasma density ``n``.

    Skw : `~astropy.units.Quantity`
        Computed spectral density function over the input ``wavelengths`` array
        with units of s/rad.

    Notes
    -----
    For details, see :cite:t:`sheffield:2011`\ . This code is a modified
    version of the program described therein.

    For a concise summary of the relevant physics, see Chapter 5 of
    :cite:t:`schaeffer:2014`\ .
    """
    if efract is None:
        efract = np.ones(1)
    else:
        efract = np.asarray(efract, dtype=np.float64)

    if ifract is None:
        ifract = np.ones(1)
    else:
        ifract = np.asarray(ifract, dtype=np.float64)

    # If electron velocity is not specified, create an array corresponding
    # to zero drift
    if electron_vel is None:
        electron_vel = np.zeros([efract.size, 3]) * u.m / u.s

    # If ion drift velocity is not specified, create an array corresponding
    # to zero drift
    if ion_vel is None:
        ion_vel = np.zeros([ifract.size, 3]) * u.m / u.s

    # Condition ion_species
    if isinstance(ion_species, (str, Particle)):
        ion_species = [ion_species]
    if len(ion_species) == 0:
        raise ValueError("At least one ion species needs to be defined.")
    for ii, ion in enumerate(ion_species):
        if isinstance(ion, Particle):
            continue
        ion_species[ii] = Particle(ion)

    # Condition Te
    if Te.size == 1:
        # If a single quantity is given, put it in an array so it's iterable
        # If Te.size != len(efract), assume same temp. for all species
        Te = np.repeat(Te, len(efract))
    elif Te.size != len(efract):
        raise ValueError(
            f"Got {Te.size} electron temperatures and expected {len(efract)}.")

    # Condition Ti
    if Ti.size == 1:
        # If a single quantity is given, put it in an array so it's iterable
        # If Ti.size != len(ion_species), assume same temp. for all species
        Ti = [Ti.value] * len(ion_species) * Ti.unit
    elif Ti.size != len(ion_species):
        raise ValueError(
            f"Got {Ti.size} ion temperatures and expected {len(ion_species)}.")

    # Make sure the sizes of ion_species, ifract, ion_vel, and Ti all match
    if ((len(ion_species) != ifract.size) or (ion_vel.shape[0] != ifract.size)
            or (Ti.size != ifract.size)):
        raise ValueError(
            f"Inconsistent number of species in ifract ({ifract}), "
            f"ion_species ({len(ion_species)}), Ti ({Ti.size}), "
            f"and/or ion_vel ({ion_vel.shape[0]}).")

    # Make sure the sizes of efract, electron_vel, and Te all match
    if (electron_vel.shape[0] != efract.size) or (Te.size != efract.size):
        raise ValueError(
            f"Inconsistent number of electron populations in efract ({efract.size}), "
            f"Te ({Te.size}), or electron velocity ({electron_vel.shape[0]}).")

    # Ensure unit vectors are normalized
    probe_vec = probe_vec / np.linalg.norm(probe_vec)
    scatter_vec = scatter_vec / np.linalg.norm(scatter_vec)

    # Define some constants
    C = const.c.si  # speed of light

    # Calculate plasma parameters
    vTe = thermal_speed(Te, particle="e-")
    vTi, ion_z = [], []
    for T, ion in zip(Ti, ion_species):
        vTi.append(thermal_speed(T, particle=ion).value)
        ion_z.append(ion.charge_number * u.dimensionless_unscaled)
    vTi = vTi * vTe.unit
    zbar = np.sum(ifract * ion_z)
    ne = efract * n
    ni = ifract * n / zbar  # ne/zbar = sum(ni)
    # wpe is calculated for the entire plasma (all electron populations combined)
    wpe = plasma_frequency(n=n, particle="e-")

    # Convert wavelengths to angular frequencies (electromagnetic waves, so
    # phase speed is c)
    ws = (2 * np.pi * u.rad * C / wavelengths).to(u.rad / u.s)
    wl = (2 * np.pi * u.rad * C / probe_wavelength).to(u.rad / u.s)

    # Compute the frequency shift (required by energy conservation)
    w = ws - wl

    # Compute the wavenumbers in the plasma
    # See Sheffield Sec. 1.8.1 and Eqs. 5.4.1 and 5.4.2
    ks = np.sqrt(ws**2 - wpe**2) / C
    kl = np.sqrt(wl**2 - wpe**2) / C

    # Compute the wavenumber shift (required by momentum conservation)
    scattering_angle = np.arccos(np.dot(probe_vec, scatter_vec))
    # Eq. 1.7.10 in Sheffield
    k = np.sqrt(ks**2 + kl**2 - 2 * ks * kl * np.cos(scattering_angle))
    # Normalized vector along k
    k_vec = (scatter_vec - probe_vec) * u.dimensionless_unscaled
    k_vec = k_vec / np.linalg.norm(k_vec)

    # Compute Doppler-shifted frequencies for both the ions and electrons
    # Matmul is simultaneously conducting dot product over all wavelengths
    # and ion components
    w_e = w - np.matmul(electron_vel, np.outer(k, k_vec).T)
    w_i = w - np.matmul(ion_vel, np.outer(k, k_vec).T)

    # Compute the scattering parameter alpha
    # expressed here using the fact that v_th/w_p = root(2) * Debye length
    alpha = np.sqrt(2) * wpe / np.outer(k, vTe)

    # Calculate the normalized phase velocities (Sec. 3.4.2 in Sheffield)
    xe = (np.outer(1 / vTe, 1 / k) * w_e).to(u.dimensionless_unscaled)
    xi = (np.outer(1 / vTi, 1 / k) * w_i).to(u.dimensionless_unscaled)

    # Calculate the susceptibilities

    chiE = np.zeros([efract.size, w.size], dtype=np.complex128)
    for i in range(len(efract)):
        chiE[i, :] = permittivity_1D_Maxwellian(w_e[i, :], k, Te[i], ne[i],
                                                "e-")

    # Treatment of multiple species is an extension of the discussion in
    # Sheffield Sec. 5.1
    chiI = np.zeros([ifract.size, w.size], dtype=np.complex128)
    for i, ion in enumerate(ion_species):
        chiI[i, :] = permittivity_1D_Maxwellian(w_i[i, :],
                                                k,
                                                Ti[i],
                                                ni[i],
                                                ion,
                                                z_mean=ion_z[i])

    # Calculate the longitudinal dielectric function
    epsilon = 1 + np.sum(chiE, axis=0) + np.sum(chiI, axis=0)

    econtr = np.zeros([efract.size, w.size], dtype=np.complex128) * u.s / u.rad
    for m in range(efract.size):
        econtr[m, :] = efract[m] * (2 * np.sqrt(np.pi) / k / vTe[m] * np.power(
            np.abs(1 - np.sum(chiE, axis=0) / epsilon), 2) *
                                    np.exp(-xe[m, :]**2))

    icontr = np.zeros([ifract.size, w.size], dtype=np.complex128) * u.s / u.rad
    for m in range(ifract.size):
        icontr[m, :] = ifract[m] * (
            2 * np.sqrt(np.pi) * ion_z[m] / k / vTi[m] *
            np.power(np.abs(np.sum(chiE, axis=0) / epsilon), 2) *
            np.exp(-xi[m, :]**2))

    # Recast as real: imaginary part is already zero
    Skw = np.real(np.sum(econtr, axis=0) + np.sum(icontr, axis=0))

    return np.mean(alpha), Skw