Esempio n. 1
0
def gen_multiple_ion_species_spectrum():
    """
    Generates an example Thomson scattering spectrum for multiple ion species
    that also have drift velocities. Parameters are set to be in the
    collective regime where ion species are important.
    """
    wavelengths = np.arange(520, 545, 0.01) * u.nm
    probe_wavelength = 532 * u.nm
    n = 5e17 * u.cm**-3
    probe_vec = np.array([1, 0, 0])
    scatter_vec = np.array([0, 1, 0])
    ifract = np.array([0.7, 0.3])
    Te = 10 * u.eV
    Ti = np.array([5, 5]) * u.eV
    electron_vel = np.array([[300, 0, 0]]) * u.km / u.s
    ion_vel = np.array([[-500, 0, 0], [0, 500, 0]]) * u.km / u.s

    # Use this to also test passing in ion species as Particle objects
    ion_species = [Particle("p+"), Particle("C-12 5+")]

    alpha, Skw = thomson.spectral_density(
        wavelengths,
        probe_wavelength,
        n,
        Te,
        Ti,
        ifract=ifract,
        ion_species=ion_species,
        probe_vec=probe_vec,
        scatter_vec=scatter_vec,
        electron_vel=electron_vel,
        ion_vel=ion_vel,
    )

    return alpha, wavelengths, Skw
Esempio n. 2
0
    def test_normal_vs_lite_values(self, inputs):
        """
        Test that plasma_frequency and plasma_frequency_lite calculate
        the same values.
        """
        particle = Particle(inputs["particle"])
        inputs_unitless = {
            "n": inputs["n"].to(u.m**-3).value,
            "mass": particle.mass.value,
        }
        if "z_mean" in inputs:
            inputs_unitless["z_mean"] = inputs["z_mean"]
        else:
            try:
                inputs_unitless["z_mean"] = np.abs(particle.charge_number)
            except Exception:
                inputs_unitless["z_mean"] = 1
        if "to_hz" in inputs:
            inputs_unitless["to_hz"] = inputs["to_hz"]

        lite = plasma_frequency_lite(**inputs_unitless)
        pylite = plasma_frequency_lite.py_func(**inputs_unitless)
        assert pylite == lite

        normal = plasma_frequency(**inputs)
        assert np.allclose(normal.value, lite)
Esempio n. 3
0
def test_ionic_fraction_attributes(ion, ionic_fraction, number_density):

    instance = IonicFraction(
        ion=ion, ionic_fraction=ionic_fraction, number_density=number_density
    )

    # Prepare to check for the default values when they are not set

    if ionic_fraction is None:
        ionic_fraction = np.nan
    if number_density is None:
        number_density = np.nan * u.m ** -3

    assert Particle(ion) == Particle(instance.ionic_symbol)
    assert u.isclose(instance.ionic_fraction, ionic_fraction, equal_nan=True)
    assert u.isclose(instance.number_density, number_density, equal_nan=True)
Esempio n. 4
0
    def test_vals_stix_figs(self, kwargs, expected):
        ion = kwargs["ions"][0]

        mu = ion.mass / Particle("e-").mass
        if not np.isclose(mu, expected["mu"], rtol=9.0e-5):
            pytest.fail(
                "Test setup failure. Check 'kwarg' parameters, given"
                f" values produces a mu of {mu:.2f} but expected "
                f"{expected['mu']:.2f}."
            )

        wpi = plasma_frequency(n=kwargs["n_i"], particle=ion).value
        wci = gyrofrequency(kwargs["B"], particle=ion).value
        gamma = (wpi / wci) ** 2
        if not np.isclose(gamma, expected["gamma"], rtol=0.3e-4):
            pytest.fail(
                "Test setup failure. Check 'kwarg' parameters, given"
                f" values produces a gamma of {gamma:.2f} but expected "
                f"{expected['gamma']:.2f}."
            )

        beta = wci / kwargs["w"].value
        if not np.isclose(beta, expected["beta"], rtol=1):
            pytest.fail(
                "Test setup failure. Check 'kwarg' parameters, given"
                f" values produces a beta of {beta:.3f} but expected "
                f"{expected['beta']:.3f}."
            )

        ks = stix(**kwargs)
        ns = ks.value * c_si_unitless / kwargs["w"].value

        assert np.allclose(ns, expected["ns"])
Esempio n. 5
0
    def spd(B, w, ions, n_i):
        w = w.to(u.rad / u.s).value
        n_i = n_i.to(u.m**-3).value

        species = ions + [Particle("e-")]
        densities = np.zeros(n_i.size + 1)
        densities[:-1] = n_i
        densities[-1] = np.sum(n_i * ions.charge_number)

        # Generate the plasma parameters needed
        wps = []
        wcs = []
        for par, dens in zip(species, densities):
            wps.append(plasma_frequency(n=dens * u.m**-3, particle=par).value)
            wcs.append(gyrofrequency(B=B, particle=par, signed=True).value)

        # Stix method implemented
        S = np.ones_like(w, dtype=np.float64)
        P = np.ones_like(S)
        D = np.zeros_like(S)
        for wc, wp in zip(wcs, wps):
            S -= (wp**2) / (w**2 - wc**2)
            P -= (wp / w) ** 2
            D += ((wp**2) / (w**2 - wc**2)) * (wc / w)

        return S, P, D
Esempio n. 6
0
 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)
     ]
Esempio n. 7
0
class Test_mass_density:
    r"""Test the mass_density function in misc.py."""
    @pytest.mark.parametrize(
        "args, kwargs, conditional",
        [
            ((-1 * u.kg * u.m**-3, "He"), {}, pytest.raises(ValueError)),
            ((-1 * u.m**-3, "He"), {}, pytest.raises(ValueError)),
            (("not a Quantity", "He"), {}, pytest.raises(TypeError)),
            ((1 * u.m**-3, ), {}, pytest.raises(TypeError)),
            ((1 * u.J, "He"), {}, pytest.raises(u.UnitTypeError)),
            ((1 * u.m**-3, None), {}, pytest.raises(TypeError)),
            (
                (1 * u.m**-3, "He"),
                {
                    "z_ratio": "not a ratio"
                },
                pytest.raises(TypeError),
            ),
        ],
    )
    def test_raises(self, args, kwargs, conditional):
        with conditional:
            mass_density(*args, **kwargs)

    @pytest.mark.parametrize(
        "args, kwargs, expected",
        [
            ((1.0 * u.g * u.m**-3, ""), {}, 1.0e-3 * u.kg * u.m**-3),
            ((5.0e12 * u.cm**-3, "He"), {}, 3.32323849e-8 * u.kg * u.m**-3),
            (
                (5.0e12 * u.cm**-3, Particle("He")),
                {},
                3.32323849e-8 * u.kg * u.m**-3,
            ),
            (
                (5.0e12 * u.cm**-3, "He"),
                {
                    "z_ratio": 0.5
                },
                1.66161925e-08 * u.kg * u.m**-3,
            ),
            (
                (5.0e12 * u.cm**-3, "He"),
                {
                    "z_ratio": -0.5
                },
                1.66161925e-08 * u.kg * u.m**-3,
            ),
        ],
    )
    def test_values(self, args, kwargs, expected):
        assert np.isclose(mass_density(*args, **kwargs), expected)

    def test_handle_nparrays(self):
        """Test for ability to handle numpy array quantities"""
        assert_can_handle_nparray(mass_density)
Esempio n. 8
0
    def test_that_particles_were_set_correctly(self, test_name):
        input_particles = tests[test_name]['inputs'].keys()
        particles = [Particle(input_particle) for input_particle in input_particles]
        expected_particles = {p.particle for p in particles}
        actual_particles = {particle for particle in self.instances[test_name].ionic_fractions.keys()}

        assert actual_particles == expected_particles, (
            f"For test='{test_name}', the following should be equal:\n"
            f"  actual_particles = {actual_particles}\n"
            f"expected_particles = {expected_particles}")
    def test_iteration(self, test_name: str):
        """Test that `IonizationState` instances iterate impeccably."""
        states = [state for state in self.instances[test_name]]

        charge_numbers = [state.charge_number for state in states]
        ionic_fractions = np.array([state.ionic_fraction for state in states])
        ionic_symbols = [state.ionic_symbol for state in states]

        try:
            base_symbol = isotope_symbol(ionic_symbols[0])
        except InvalidIsotopeError:
            base_symbol = atomic_symbol(ionic_symbols[0])
        finally:
            atomic_numb = atomic_number(ionic_symbols[1])

        errors = []

        expected_charges = np.arange(atomic_numb + 1)
        if not np.all(charge_numbers == expected_charges):
            errors.append(
                f"The resulting charge numbers are {charge_numbers}, "
                f"which are not equal to the expected charge numbers, "
                f"which are {expected_charges}.")

        expected_fracs = test_cases[test_name]["ionic_fractions"]
        if isinstance(expected_fracs, u.Quantity):
            expected_fracs = (expected_fracs / expected_fracs.sum()).value

        if not np.allclose(ionic_fractions, expected_fracs):
            errors.append(
                f"The resulting ionic fractions are {ionic_fractions}, "
                f"which are not equal to the expected ionic fractions "
                f"of {expected_fracs}.")

        expected_particles = [
            Particle(base_symbol, Z=charge) for charge in charge_numbers
        ]
        expected_symbols = [
            particle.ionic_symbol for particle in expected_particles
        ]
        if not ionic_symbols == expected_symbols:
            errors.append(
                f"The resulting ionic symbols are {ionic_symbols}, "
                f"which are not equal to the expected ionic symbols of "
                f"{expected_symbols}.")

        if errors:
            errors.insert(
                0,
                (f"The test of IonizationState named '{test_name}' has "
                 f"resulted in the following errors when attempting to "
                 f"iterate."),
            )
            errmsg = " ".join(errors)
            pytest.fail(errmsg)
Esempio n. 10
0
class TestThermalSpeedLite:
    """Test class for `thermal_speed_lite`."""
    def test_is_jitted(self):
        """Ensure `thermal_speed_lite` was jitted by `numba`."""
        assert is_jitted(thermal_speed_lite)

    @pytest.mark.parametrize(
        "inputs",
        [
            dict(T=5 * u.eV,
                 particle=Particle("p"),
                 method="most_probable",
                 ndim=3),
            dict(T=3000 * u.K, particle=Particle("e"), method="nrl", ndim=2),
            dict(T=5000 * u.K,
                 particle=Particle("He+"),
                 method="mean_magnitude",
                 ndim=1),
            dict(T=1 * u.eV, particle=Particle("Ar+"), method="rms", ndim=3),
        ],
    )
    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)
    def test_that_ionic_fractions_are_set_correctly(self, test_name):

        errmsg = ""

        elements_actual = self.instances[test_name].base_particles
        inputs = tests[test_name]["inputs"]

        if isinstance(inputs, dict):
            input_keys = list(tests[test_name]["inputs"].keys())

            input_keys = sorted(
                input_keys,
                key=lambda k: (
                    atomic_number(k),
                    mass_number(k) if Particle(k).isotope else 0,
                ),
            )

            for element, input_key in zip(elements_actual, input_keys):
                expected = tests[test_name]["inputs"][input_key]

                if isinstance(expected, u.Quantity):
                    expected = np.array(expected.value /
                                        np.sum(expected.value))

                actual = self.instances[test_name].ionic_fractions[element]

                if not np.allclose(actual, expected):
                    errmsg += (
                        f"\n\nThere is a discrepancy in ionic fractions for "
                        f"({test_name}, {element}, {input_key})\n"
                        f"  expected = {expected}\n"
                        f"    actual = {actual}")

                if not isinstance(actual, np.ndarray) or isinstance(
                        actual, u.Quantity):
                    raise ParticleError(
                        f"\n\nNot a numpy.ndarray: ({test_name}, {element})")
        else:
            elements_expected = {
                particle_symbol(element)
                for element in inputs
            }

            assert set(
                self.instances[test_name].base_particles) == elements_expected

            for element in elements_expected:
                assert all(
                    np.isnan(
                        self.instances[test_name].ionic_fractions[element]))
        if errmsg:
            pytest.fail(errmsg)
Esempio n. 12
0
    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
Esempio n. 13
0
def test_ionic_level_attributes(ion, ionic_fraction, number_density):

    instance = IonicLevel(ion=ion,
                          ionic_fraction=ionic_fraction,
                          number_density=number_density)

    # Prepare to check for the default values when they are not set

    if ionic_fraction is None:
        ionic_fraction = np.nan
    if number_density is None:
        number_density = np.nan * u.m**-3

    assert Particle(ion) == Particle(instance.ionic_symbol)
    assert u.isclose(instance.ionic_fraction, ionic_fraction, equal_nan=True)
    assert u.isclose(instance.number_density, number_density, equal_nan=True)
    assert instance.charge_number == charge_number(ion)

    # TODO: remove when IonicLevel.integer_charge is removed
    with pytest.warns(PlasmaPyFutureWarning):
        integer_charge = instance.integer_charge
    assert integer_charge == charge_number(ion)
Esempio n. 14
0
 def test_particle_instances(self, test_name):
     """
     Test that `IonizationState` returns the correct `Particle`
     instances.
     """
     instance = self.instances[test_name]
     atom = instance.base_particle
     nstates = instance.atomic_number + 1
     expected_particles = [Particle(atom, Z=Z) for Z in range(nstates)]
     assert expected_particles == instance._particle_instances, (
         f"The expected Particle instances of {expected_particles} "
         f"are not all equal to the IonizationState particles of "
         f"{instance._particle_instances} for test {test_name}.")
Esempio n. 15
0
def args_to_lite_args(kwargs):
    """
    Converts a dict of args for the spectral density function and converts
    them to input for the lite function.

    Used to facilitate testing the two functions against each other.
    """
    keys = list(kwargs.keys())

    if "wavelengths" in keys:
        kwargs["wavelengths"] = kwargs["wavelengths"].to(u.m).value
    if "probe_wavelength" in keys:
        kwargs["probe_wavelength"] = kwargs["probe_wavelength"].to(u.m).value
    if "n" in keys:
        kwargs["n"] = kwargs["n"].to(u.m**-3).value
    if "T_e" in keys:
        kwargs["T_e"] = (kwargs["T_e"] / const.k_B).to(u.K).value
    if "T_i" in keys:
        kwargs["T_i"] = (kwargs["T_i"] / const.k_B).to(u.K).value
    if "electron_vel" in keys:
        kwargs["electron_vel"] = kwargs["electron_vel"].to(u.m / u.s).value
    if "ion_vel" in keys:
        kwargs["ion_vel"] = kwargs["ion_vel"].to(u.m / u.s).value

    if kwargs["T_e"].size == 1:
        kwargs["T_e"] = np.array([
            kwargs["T_e"],
        ])
    if kwargs["T_i"].size == 1:
        kwargs["T_i"] = np.array([
            kwargs["T_i"],
        ])

    if not isinstance(kwargs["ions"], list):
        kwargs["ions"] = [
            kwargs["ions"],
        ]

    ion_z = np.zeros(len(kwargs["ions"]))
    ion_mass = np.zeros(len(kwargs["ions"]))
    for i, particle in enumerate(kwargs["ions"]):
        if not isinstance(particle, Particle):
            particle = Particle(particle)
        ion_z[i] = particle.charge_number
        ion_mass[i] = particle_mass(particle).to(u.kg).value
    kwargs["ion_z"] = ion_z
    kwargs["ion_mass"] = ion_mass
    del kwargs["ions"]

    return kwargs
Esempio n. 16
0
def multiple_species_collective_args():
    """
    Standard args

    Includes both kwargs and args: separated by the function

    spectral_density_args_kwargs

    """
    kwargs = {}
    kwargs["wavelengths"] = np.arange(520, 545, 0.01) * u.nm
    kwargs["probe_wavelength"] = 532 * u.nm
    kwargs["n"] = 5e17 * u.cm**-3
    kwargs["T_e"] = 10 * u.eV
    kwargs["T_i"] = np.array([5, 5]) * u.eV
    kwargs["ions"] = [Particle("p+"), Particle("C-12 5+")]
    kwargs["probe_vec"] = np.array([1, 0, 0])
    kwargs["scatter_vec"] = np.array([0, 1, 0])
    kwargs["efract"] = np.array([1.0])
    kwargs["ifract"] = np.array([0.7, 0.3])
    kwargs["electron_vel"] = np.array([[300, 0, 0]]) * u.km / u.s
    kwargs["ion_vel"] = np.array([[-500, 0, 0], [0, 500, 0]]) * u.km / u.s

    return kwargs
Esempio n. 17
0
def test_IonizationState_ionfracs_from_ion_input(ion):

    ionization_state = IonizationState(ion)
    ion_particle = Particle(ion)
    actual_ionic_fractions = ionization_state.ionic_fractions

    expected_ionic_fractions = np.zeros(ion_particle.atomic_number + 1)
    expected_ionic_fractions[ion_particle.integer_charge] = 1.0

    if not np.allclose(expected_ionic_fractions, actual_ionic_fractions, atol=1e-16):
        pytest.fail(
            f"The returned ionic fraction for IonizationState({repr(ion)}) "
            f"should have entirely been in the Z = {ion_particle.integer_charge} "
            f"level, but was instead: {ionization_state.ionic_fractions}."
        )
Esempio n. 18
0
    def try_change_value(self, value):
        """
        Set property_name in values_container to value,
        and resets the error status.

        Parameters
        ----------
        value: `str`
            Value to be set

        Raises
        ------
        `~plasmapy.particles.exceptions.InvalidParticleError`
            Raised when the particle input does not correspond to a valid
            particle or is contradictory.
        """
        particle = Particle(value)
        self.values_cont[self.property_name] = particle
        self.widget.layout.border = ""
        self.widget.description = ""
Esempio n. 19
0
def test_IonizationState_base_particles_from_ion_input(ion):
    """
    Test that supplying an ion to IonizationState will result in the
    base particle being the corresponding isotope or ion and that the
    ionic fraction of the corresponding charge level is 100%.
    """

    ionization_state = IonizationState(ion)
    ion_particle = Particle(ion)

    if ion_particle.isotope:
        expected_base_particle = ion_particle.isotope
    else:
        expected_base_particle = ion_particle.element

    if expected_base_particle != ionization_state.base_particle:
        pytest.fail(
            f"The expected base particle was {expected_base_particle}, "
            f"but the returned base particle was {ionization_state.base_particle}. "
        )
Esempio n. 20
0
    def try_change_value(self, value):
        """
        Set property_name in values_container to value on validating input.

        Parameters
        ----------
        value: `str`
            Value to be set

        Raises
        ------
        `~plasmapy.particles.exceptions.InvalidParticleError`
            Raised when the particle input does not correspond to a valid
            particle or is contradictory.
        `ValueError`
            Raised when the input is not a valid ion
        """
        ion = Particle(value)
        if not ion.is_ion:
            raise ValueError(f"{ion} is not an ion")

        self.values_cont[self.property_name] = ion
        self.widget.layout.border = ""
        self.widget.description = ""
Esempio n. 21
0
def two_fluid(
    *,
    B: u.T,
    ion: Union[str, Particle],
    k: u.rad / u.m,
    n_i: u.m**-3,
    T_e: u.K,
    T_i: u.K,
    theta: u.rad,
    gamma_e: Union[float, int] = 1,
    gamma_i: Union[float, int] = 3,
    z_mean: Union[float, int] = None,
):
    r"""
    Using the solution provided by :cite:t:`bellan:2012`, calculate the
    analytical solution to the two fluid, low-frequency
    (:math:`\omega/kc \ll 1`) dispersion relation presented by
    :cite:t:`stringer:1963`.  This dispersion relation also assumes a
    uniform magnetic field :math:`\mathbf{B_o}`, no D.C. electric field
    :math:`\mathbf{E_o}=0`, and quasi-neutrality.  For more information
    see the **Notes** section below.

    Parameters
    ----------
    B : `~astropy.units.Quantity`
        The magnetic field magnitude in units convertible to T.
    ion : `str` or `~plasmapy.particles.particle_class.Particle`
        Representation of the ion species (e.g., ``'p'`` for protons,
        ``'D+'`` for deuterium, ``'He-4 +1'`` for singly ionized
        helium-4, etc.). If no charge state information is provided,
        then the ions are assumed to be singly ionized.
    k : `~astropy.units.Quantity`, single valued or 1-D array
        Wavenumber in units convertible to rad/m`.  Either single
        valued or 1-D array of length :math:`N`.
    n_i : `~astropy.units.Quantity`
        Ion number density in units convertible to m\ :sup:`-3`.
    T_e : `~astropy.units.Quantity`
        The electron temperature in units of K or eV.
    T_i : `~astropy.units.Quantity`
        The ion temperature in units of K or eV.
    theta : `~astropy.units.Quantity`, single valued or 1-D array
        The angle of propagation of the wave with respect to the
        magnetic field, :math:`\cos^{-1}(k_z / k)`, in units must be
        convertible to radians. Either single valued or 1-D array of
        size :math:`M`.
    gamma_e : `float` or `int`, optional
        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`, optional
        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.
    z_mean : `float` or int, optional
        The average ionization state (arithmetic mean) of the ``ion``
        composing the plasma.  Will override any charge state defined
        by argument ``ion``.

    Returns
    -------
    omega : Dict[str, `~astropy.units.Quantity`]
        A dictionary of computed wave frequencies in units rad/s.  The
        dictionary contains three keys: ``'fast_mode'`` for the fast
        mode, ``'alfven_mode'`` for the Alfvén mode, and
        ``'acoustic_mode'`` for the ion-acoustic mode.  The value for
        each key will be a :math:`N × M` array.

    Raises
    ------
    TypeError
        If applicable arguments are not instances of
        `~astropy.units.Quantity` or cannot be converted into one.

    TypeError
        If ``ion`` is not of type or convertible to
        `~plasmapy.particles.particle_class.Particle`.

    TypeError
        If ``gamma_e``, ``gamma_i``, or ``z_mean`` are not of type `int`
        or `float`.

    ~astropy.units.UnitTypeError
        If applicable arguments do not have units convertible to the
        expected units.

    ValueError
        If any of ``B``, ``k``, ``n_i``, ``T_e``, or ``T_i`` is negative.

    ValueError
        If ``k`` is negative or zero.

    ValueError
        If ``ion`` is not of category ion or element.

    ValueError
        If ``B``, ``n_i``, ``T_e``, or ``T_I`` are not single valued
        `astropy.units.Quantity` (i.e. an array).

    ValueError
        If ``k`` or ``theta`` are not single valued or a 1-D array.

    Warns
    -----
    : `~plasmapy.utils.exceptions.PhysicsWarning`
        When the computed wave frequencies violate the low-frequency
        (:math:`\omega/kc \ll 1`) assumption of the dispersion relation.

    Notes
    -----

    The complete dispersion equation presented by :cite:t:`stringer:1963`
    (equation 1 of :cite:t:`bellan:2012`) is:

    .. math::
        \left( \cos^2 \theta - Q \frac{\omega^2}{k^2 {v_A}^2} \right) &
        \left[
            \left( \cos^2 \theta - \frac{\omega^2}{k^2 {c_s}^2} \right)
            - Q \frac{\omega^2}{k^2 {v_A}^2} \left(
                1 - \frac{\omega^2}{k^2 {c_s}^2}
            \right)
        \right] \\
            &= \left(1 - \frac{\omega^2}{k^2 {c_s}^2} \right)
              \frac{\omega^2}{{\omega_{ci}}^2} \cos^2 \theta

    where

    .. math::
        Q &= 1 + k^2 c^2/{\omega_{pe}}^2 \\
        \cos \theta &= \frac{k_z}{k} \\
        \mathbf{B_o} &= B_{o} \mathbf{\hat{z}}

    :math:`\omega` is the wave frequency, :math:`k` is the wavenumber,
    :math:`v_A` is the Alfvén velocity, :math:`c_s` is the sound speed,
    :math:`\omega_{ci}` is the ion gyrofrequency, and
    :math:`\omega_{pe}` is the electron plasma frequency. This relation
    does additionally assume low-frequency waves
    :math:`\omega/kc \ll 1`, no D.C. electric field
    :math:`\mathbf{E_o}=0` and quasi-neutrality.

    Following section 5 of :cite:t:`bellan:2012` the exact roots of the
    above dispersion equation can be derived and expressed as one
    analytical solution (equation 38 of :cite:t:`bellan:2012`):

    .. math::
        \frac{\omega}{\omega_{ci}} = \sqrt{
            2 \Lambda \sqrt{-\frac{P}{3}} \cos\left(
                \frac{1}{3} \cos^{-1}\left(
                    \frac{3q}{2p} \sqrt{-\frac{3}{p}}
                \right)
                - \frac{2 \pi}{3}j
            \right)
            + \frac{\Lambda A}{3}
        }

    where :math:`j = 0` represents the fast mode, :math:`j = 1`
    represents the Alfvén mode, and :math:`j = 2` represents the
    acoustic mode.  Additionally,

    .. math::
        p &= \frac{3B-A^2}{3} \; , \; q = \frac{9AB-2A^3-27C}{27} \\
        A &= \frac{Q + Q^2 \beta + Q \alpha + \alpha \Lambda}{Q^2} \;
            , \; B = \alpha \frac{1 + 2 Q \beta + \Lambda \beta}{Q^2} \;
            , \; C = \frac{\alpha^2 \beta}{Q^2} \\
        \alpha &= \cos^2 \theta \;
            , \; \beta = \left( \frac{c_s}{v_A}\right)^2 \;
            , \; \Lambda = \left( \frac{k v_{A}}{\omega_{ci}}\right)^2

    Examples
    --------
    >>> from astropy import units as u
    >>> from plasmapy.dispersion.analytical import two_fluid
    >>> inputs = {
    ...     "k": 0.01 * u.rad / u.m,
    ...     "theta": 30 * u.deg,
    ...     "B": 8.3e-9 * u.T,
    ...     "n_i": 5e6 * u.m ** -3,
    ...     "T_e": 1.6e6 * u.K,
    ...     "T_i": 4.0e5 * u.K,
    ...     "ion": "p+",
    ... }
    >>> omegas = two_fluid(**inputs)
    >>> omegas
    {'fast_mode': <Quantity 1520.57... rad / s>,
     'alfven_mode': <Quantity 1261.75... rad / s>,
     'acoustic_mode': <Quantity 0.688152... rad / s>}

    >>> inputs = {
    ...     "k": [1e-7, 2e-7] * u.rad / u.m,
    ...     "theta": [10, 20] * u.deg,
    ...     "B": 8.3e-9 * u.T,
    ...     "n_i": 5e6 * u.m ** -3,
    ...     "T_e": 1.6e6 * u.K,
    ...     "T_i": 4.0e5 * u.K,
    ...     "ion": "He+",
    ... }
    >>> omegas = two_fluid(**inputs)
    >>> omegas['fast_mode']
    <Quantity [[0.00767..., 0.00779... ],
               [0.01534..., 0.01558...]] rad / s>
    """

    # validate argument ion
    if not isinstance(ion, Particle):
        try:
            ion = Particle(ion)
        except TypeError:
            raise TypeError(
                f"For argument 'ion' expected type {Particle} but got {type(ion)}."
            )
    if not ion.is_ion and not ion.is_category("element"):
        raise ValueError(
            "The particle passed for 'ion' must be an ion or element.")

    # validate z_mean
    if z_mean is None:
        try:
            z_mean = abs(ion.charge_number)
        except ChargeError:
            z_mean = 1
    elif isinstance(z_mean, (int, np.integer, float, np.floating)):
        z_mean = abs(z_mean)
    else:
        raise TypeError(
            f"Expected int or float for argument 'z_mean', but got {type(z_mean)}."
        )

    # validate arguments
    for arg_name in ("B", "n_i", "T_e", "T_i"):
        val = locals()[arg_name].squeeze()
        if val.shape != ():
            raise ValueError(
                f"Argument '{arg_name}' must a single value and not an array of "
                f"shape {val.shape}.")
        locals()[arg_name] = val

    # validate arguments
    for arg_name in ("gamma_e", "gamma_i"):
        if not isinstance(locals()[arg_name],
                          (int, np.integer, float, np.floating)):
            raise TypeError(
                f"Expected int or float for argument '{arg_name}', but got "
                f"{type(locals()[arg_name])}.")

    # validate argument k
    k = k.squeeze()
    if k.ndim not in [0, 1]:
        raise ValueError(
            f"Argument 'k' needs to be a single valued or 1D array astropy Quantity,"
            f" got array of shape {k.shape}.")
    if np.any(k <= 0):
        raise ValueError("Argument 'k' can not be a or have negative values.")

    # validate argument theta
    theta = theta.squeeze()
    if theta.ndim not in [0, 1]:
        raise ValueError(
            f"Argument 'theta' needs to be a single valued or 1D array astropy "
            f"Quantity, got array of shape {k.shape}.")

    # Calc needed plasma parameters
    n_e = z_mean * n_i
    with warnings.catch_warnings():
        warnings.simplefilter("ignore", category=PhysicsWarning)
        c_s = ion_sound_speed(
            T_e=T_e,
            T_i=T_i,
            ion=ion,
            n_e=n_e,
            gamma_e=gamma_e,
            gamma_i=gamma_i,
            z_mean=z_mean,
        )
    v_A = Alfven_speed(B, n_i, ion=ion, z_mean=z_mean)
    omega_ci = gyrofrequency(B=B, particle=ion, signed=False, Z=z_mean)
    omega_pe = plasma_frequency(n=n_e, particle="e-")

    # Bellan2012JGR params equation 32
    alpha = np.cos(theta.value)**2
    beta = (c_s / v_A).to(u.dimensionless_unscaled).value**2
    alphav, kv = np.meshgrid(alpha, k.value)  # create grid
    Lambda = (kv * v_A.value / omega_ci.value)**2

    # Bellan2012JGR params equation 2
    Q = 1 + (kv * c.value / omega_pe.value)**2

    # Bellan2012JGR params equation 35
    A = ((1 + alphav) / Q) + beta + (alphav * Lambda / Q**2)
    B = alphav * (1 + 2 * Q * beta + Lambda * beta) / Q**2
    C = beta * (alphav / Q)**2

    # Bellan2012JGR params equation 36
    p = (3 * B - A**2) / 3
    q = (9 * A * B - 2 * A**3 - 27 * C) / 27

    # Bellan2012JGR params equation 38
    R = 2 * Lambda * np.emath.sqrt(-p / 3)
    S = 3 * q / (2 * p) * np.emath.sqrt(-3 / p)
    T = Lambda * A / 3
    omega = {}
    for ind, key in enumerate(("fast_mode", "alfven_mode", "acoustic_mode")):
        # The solution corresponding to equation 38
        w = omega_ci * np.emath.sqrt(
            R * np.cos(1 / 3 * np.emath.arccos(S) - 2 * np.pi / 3 * ind) + T)
        omega[key] = w.squeeze()

        # check for violation of dispersion relation assumptions
        # (i.e. low-frequency, w/kc << 0.1)
        wkc_max = np.max(w.value / (kv * c.value))
        if wkc_max > 0.1:
            warnings.warn(
                f"The {key} calculation produced a high-frequency wave (w/kc == "
                f"{wkc_max:.3f}), which violates the low-frequency (w/kc << 1) "
                f"assumption of the dispersion relation.",
                PhysicsWarning,
            )

    return omega
Esempio n. 22
0
class TestStix:
    _kwargs_single_valued = {
        "B": 8.3e-9 * u.T,
        "w": 0.001 * u.rad / u.s,
        "ions": [Particle("He+"), Particle("H+")],
        "n_i": [4.0e5, 2.0e5] * u.m**-3,
        "theta": 30 * u.deg,
    }

    @staticmethod
    def spd(B, w, ions, n_i):
        w = w.to(u.rad / u.s).value
        n_i = n_i.to(u.m**-3).value

        species = ions + [Particle("e-")]
        densities = np.zeros(n_i.size + 1)
        densities[:-1] = n_i
        densities[-1] = np.sum(n_i * ions.charge_number)

        # Generate the plasma parameters needed
        wps = []
        wcs = []
        for par, dens in zip(species, densities):
            wps.append(plasma_frequency(n=dens * u.m**-3, particle=par).value)
            wcs.append(gyrofrequency(B=B, particle=par, signed=True).value)

        # Stix method implemented
        S = np.ones_like(w, dtype=np.float64)
        P = np.ones_like(S)
        D = np.zeros_like(S)
        for wc, wp in zip(wcs, wps):
            S -= (wp**2) / (w**2 - wc**2)
            P -= (wp / w) ** 2
            D += ((wp**2) / (w**2 - wc**2)) * (wc / w)

        return S, P, D

    @pytest.mark.parametrize(
        "kwargs, _error",
        [
            ({**_kwargs_single_valued, "B": "wrong type"}, TypeError),
            ({**_kwargs_single_valued, "B": [8e-9, 8.5e-9] * u.T}, ValueError),
            ({**_kwargs_single_valued, "B": -1 * u.T}, ValueError),
            ({**_kwargs_single_valued, "B": 5 * u.m}, u.UnitTypeError),
            ({**_kwargs_single_valued, "w": -1.0 * u.rad / u.s}, ValueError),
            ({**_kwargs_single_valued, "w": [-1, 2] * u.rad / u.s}, ValueError),
            ({**_kwargs_single_valued, "w": np.ones((2, 2)) * u.rad / u.s}, ValueError),
            ({**_kwargs_single_valued, "w": 5 * u.s}, u.UnitTypeError),
            (
                {**_kwargs_single_valued, "ions": {"not": "a particle"}},
                InvalidParticleError,
            ),
            ({**_kwargs_single_valued, "n_i": "wrong type"}, TypeError),
            ({**_kwargs_single_valued, "n_i": 6 * u.m / u.s}, u.UnitTypeError),
            ({**_kwargs_single_valued, "theta": 5 * u.eV}, u.UnitTypeError),
            (
                {**_kwargs_single_valued, "theta": np.ones((2, 2)) * u.rad},
                TypeError,
            ),
            ({**_kwargs_single_valued, "ions": Particle("e-")}, ValueError),
            ({**_kwargs_single_valued, "n_i": [4, 2, 3] * u.m**-3}, ValueError),
            (
                {**_kwargs_single_valued, "n_i": np.ones((2, 2)) * u.m**-3},
                ValueError,
            ),
        ],
    )
    def test_raises(self, kwargs, _error):
        with pytest.raises(_error):
            stix(**kwargs)

    @pytest.mark.parametrize(
        "kwargs, expected",
        [
            ({**_kwargs_single_valued, "w": 2 * u.rad / u.s}, {"shape": (4,)}),
            (
                {**_kwargs_single_valued, "w": [10] * u.rad / u.s},
                {"shape": (4,)},
            ),
            (
                {**_kwargs_single_valued, "w": [10, 20, 30] * u.rad / u.s},
                {"shape": (3, 4)},
            ),
            ({**_kwargs_single_valued, "ions": ["He+", "H+"]}, {"shape": (4,)}),
            (
                {
                    **_kwargs_single_valued,
                    "ions": ["He+"],
                    "n_i": [1] * u.m**-3,
                },
                {"shape": (4,)},
            ),
            ({**_kwargs_single_valued, "ions": ["He+", "H+"]}, {"shape": (4,)}),
            (
                {
                    **_kwargs_single_valued,
                    "ions": ["He+", "H+"],
                    "n_i": [1, 2] * u.m**-3,
                },
                {"shape": (4,)},
            ),
            ({**_kwargs_single_valued, "theta": [10, 20, 30]}, {"shape": (3, 4)}),
            (
                {
                    **_kwargs_single_valued,
                    "w": [10, 20] * u.rad / u.s,
                    "theta": 10 * u.rad,
                },
                {"shape": (2, 4)},
            ),
            (
                {
                    **_kwargs_single_valued,
                    "w": [10, 20] * u.rad / u.s,
                    "theta": [0, np.pi / 2, np.pi] * u.rad,
                },
                {"shape": (2, 3, 4)},
            ),
        ],
    )
    def test_return_structure(self, kwargs, expected):
        k = stix(**kwargs)

        assert isinstance(k, u.Quantity)
        assert np.shape(k) == expected["shape"]
        assert k.unit == u.rad / u.m

    @pytest.mark.parametrize(
        "kwargs, expected",
        [
            # case taken from Stix figure 1-1
            # Note: ns = [n = k * c / w, ]
            (
                {
                    "theta": 0 * u.rad,
                    "ions": [Particle("p")],
                    "n_i": 1e12 * u.cm**-3,
                    "B": 0.434634 * u.T,
                    "w": 4.16321e4 * u.rad / u.s,
                },
                {
                    "gamma": 1000,
                    "beta": 1000,
                    "mu": 1836,
                    "ns": np.array([31.63146, -31.63146, 31.66306, -31.66306]),
                },
            ),
            #
            # case taken from Stix figure 1-2
            (
                {
                    "theta": 0 * u.rad,
                    "ions": [Particle("p")],
                    "n_i": 1e12 * u.cm**-3,
                    "B": 0.434634 * u.T,
                    "w": (41632 * 10**3) * u.rad / u.s,
                },
                {
                    "gamma": 1000,
                    "beta": 1.1,
                    "mu": 1836,
                    "ns": np.array(
                        [22.39535727, -22.39535727, 6934.82540607, -6934.82540607]
                    ),
                },
            ),
            #
            # case taken from Stix figure 1-3
            (
                {
                    "theta": 0 * u.rad,
                    "ions": [Particle("p")],
                    "n_i": 1e12 * u.cm**-3,
                    "B": 0.434634 * u.T,
                    "w": (124896 * 10**5) * u.rad / u.s,
                },
                {
                    "gamma": 1000,
                    "beta": 1 / 400,
                    "mu": 1836,
                    "ns": np.array(
                        [0.0 + 1.36982834j, 0.0 - 1.36982834j, 2.23009361, -2.23009361]
                    ),
                },
            ),
            #
            # case taken from Stix figure 1-4
            (
                {
                    "theta": 0 * u.rad,
                    "ions": [Particle("p")],
                    "n_i": 1e12 * u.cm**-3,
                    "B": 0.434634 * u.T,
                    "w": (4136 * 10**7) * u.rad / u.s,
                },
                {
                    "gamma": 1000,
                    "beta": 1 / 1300,
                    "mu": 1836,
                    "ns": np.array([0.5880416, -0.5880416, 1.78668573, -1.78668573]),
                },
            ),
            #
            # case taken from Stix figure 1-5
            (
                {
                    "theta": 0 * u.rad,
                    "ions": [Particle("p")],
                    "n_i": 1e12 * u.cm**-3,
                    "B": 0.434634 * u.T,
                    "w": (4136 * 10**7) * u.rad / u.s,
                },
                {
                    "gamma": 1000,
                    "beta": 1 / 1418.5,
                    "mu": 1836,
                    "ns": np.array([0.5880416, -0.5880416, 1.78668573, -1.78668573]),
                },
            ),
        ],
    )
    def test_vals_stix_figs(self, kwargs, expected):
        ion = kwargs["ions"][0]

        mu = ion.mass / Particle("e-").mass
        if not np.isclose(mu, expected["mu"], rtol=9.0e-5):
            pytest.fail(
                "Test setup failure. Check 'kwarg' parameters, given"
                f" values produces a mu of {mu:.2f} but expected "
                f"{expected['mu']:.2f}."
            )

        wpi = plasma_frequency(n=kwargs["n_i"], particle=ion).value
        wci = gyrofrequency(kwargs["B"], particle=ion).value
        gamma = (wpi / wci) ** 2
        if not np.isclose(gamma, expected["gamma"], rtol=0.3e-4):
            pytest.fail(
                "Test setup failure. Check 'kwarg' parameters, given"
                f" values produces a gamma of {gamma:.2f} but expected "
                f"{expected['gamma']:.2f}."
            )

        beta = wci / kwargs["w"].value
        if not np.isclose(beta, expected["beta"], rtol=1):
            pytest.fail(
                "Test setup failure. Check 'kwarg' parameters, given"
                f" values produces a beta of {beta:.3f} but expected "
                f"{expected['beta']:.3f}."
            )

        ks = stix(**kwargs)
        ns = ks.value * c_si_unitless / kwargs["w"].value

        assert np.allclose(ns, expected["ns"])

    @pytest.mark.parametrize(
        "kwargs",
        [
            {
                "ions": ParticleList([Particle("p")]),
                "n_i": [1e12] * u.cm**-3,
                "B": 0.434634 * u.T,
                "w": 4136e7 * u.rad / u.s,
            },
            {
                "ions": ParticleList([Particle("p")]),
                "n_i": [1e12] * u.cm**-3,
                "B": 0.300 * u.T,
                "w": 6e5 * u.rad / u.s,
            },
            {
                "ions": ParticleList([Particle("p")]),
                "n_i": [1e12] * u.cm**-3,
                "B": 0.300 * u.T,
                "w": np.linspace(6e5, 1e9, 10) * u.rad / u.s,
            },
            {
                "ions": ParticleList([Particle("p"), Particle("He+")]),
                "n_i": [
                    0.3 * 1e13,
                    0.7 * 1e13,
                ]
                * u.cm**-3,
                "B": 0.400 * u.T,
                "w": np.linspace(6e5, 1e9, 10) * u.rad / u.s,
            },
        ],
    )
    def test_vals_theta_zero(self, kwargs):
        """
        Test on the known solutions for theta = 0,
        see Stix ch. 1 eqn 37.
        """
        S, P, D = self.spd(**kwargs)
        R = S + D
        L = S - D

        ks = stix(**{**kwargs, "theta": 0 * u.rad})
        ns = ks.value * c_si_unitless / np.tile(kwargs["w"].value, (4, 1)).transpose()

        n_soln1 = np.emath.sqrt(0.5 * (R + L - np.abs(R - L)))  # n^2 = L
        n_soln2 = np.emath.sqrt(0.5 * (R + L + np.abs(R - L)))  # n^2 = R

        assert np.allclose(ns[..., 0], n_soln1)
        assert np.allclose(ns[..., 1], -n_soln1)
        assert np.allclose(ns[..., 2], n_soln2)
        assert np.allclose(ns[..., 3], -n_soln2)

    @pytest.mark.parametrize(
        "kwargs",
        [
            {
                "ions": ParticleList([Particle("p")]),
                "n_i": [1e12] * u.cm**-3,
                "B": 0.434634 * u.T,
                "w": 4136e7 * u.rad / u.s,
            },
            {
                "ions": ParticleList([Particle("p")]),
                "n_i": [1e12] * u.cm**-3,
                "B": 0.300 * u.T,
                "w": 6e5 * u.rad / u.s,
            },
            {
                "ions": ParticleList([Particle("p")]),
                "n_i": [1e12] * u.cm**-3,
                "B": 0.300 * u.T,
                "w": np.linspace(6e5, 1e9, 10) * u.rad / u.s,
            },
            {
                "ions": ParticleList([Particle("p"), Particle("He+")]),
                "n_i": [
                    0.3 * 1e13,
                    0.7 * 1e13,
                ]
                * u.cm**-3,
                "B": 0.400 * u.T,
                "w": np.linspace(6e5, 1e9, 10) * u.rad / u.s,
            },
        ],
    )
    def test_vals_theta_90deg(self, kwargs):
        """
        Test on the known solutions for theta = pi/2,
        see Stix ch. 1 eqn 38.
        """
        S, P, D = self.spd(**kwargs)
        R = S + D
        L = S - D

        ks = stix(**{**kwargs, "theta": 0.5 * np.pi * u.rad})
        ns = ks.value * c_si_unitless / np.tile(kwargs["w"].value, (4, 1)).transpose()

        n_soln1 = np.emath.sqrt(
            (R * L + P * S + np.abs(R * L - P * S)) / (2 * S)
        )  # n^2 = RL / S
        n_soln2 = np.emath.sqrt(
            (R * L + P * S - np.abs(R * L - P * S)) / (2 * S)
        )  # n^2 = P

        assert np.allclose(ns[..., 0], n_soln1)
        assert np.allclose(ns[..., 1], -n_soln1)
        assert np.allclose(ns[..., 2], n_soln2)
        assert np.allclose(ns[..., 3], -n_soln2)
Esempio n. 23
0
def swept_probe_analysis(
    probe_characteristic,
    probe_area: u.m ** 2,
    gas_argument,
    bimaxwellian=False,
    visualize=False,
    plot_electron_fit=False,
    plot_EEDF=False,
):
    r"""Attempt to perform a basic swept probe analysis based on the provided
    characteristic and probe data. Suitable for single cylindrical probes in
    low-pressure DC plasmas, since OML is applied.

    Parameters
    ----------
    probe_characteristic : ~plasmapy.diagnostics.langmuir.Characteristic
        The swept probe characteristic that is to be analyzed.

    probe_area : ~astropy.units.Quantity
        The area of the probe exposed to plasma in units convertible to m^2.

    gas_argument : argument to instantiate the `Particle` class.
        `str`, `int`, or `~plasmapy.particles.Particle`
        A string representing a particle, element, isotope, or ion; an
        integer representing the atomic number of an element; or a
        `Particle` instance.

    visualize : bool, optional
        Can be used to plot the characteristic and the obtained parameters.
        Default is False.

    plot_electron_fit : bool, optional
        If True, the fit of the electron current in the exponential section is
        shown. Default is False.

    plot_EEDF : bool, optional
        If True, the EEDF is computed and shown. Default is False.

    Returns
    -------

    Results are returned as Dictionary

    "T_e" : `astropy.units.Quantity`
        Best estimate of the electron temperature in units of eV. Contains
        two values if bimaxwellian is True.

    "n_e" : `astropy.units.Quantity`
        Estimate of the electron density in units of m^-3. See the Notes on
        plasma densities.

    "n_i" : `astropy.units.Quantity`
        Estimate of the ion density in units of m^-3. See the Notes on
        plasma densities.

    "n_i_OML" : `astropy.units.Quantity`
        OML-theory estimate of the ion density in units of m^-3. See the Notes
        on plasma densities.

    "V_F" : `astropy.units.Quantity`
        Estimate of the floating potential in units of V.

    "V_P" : `astropy.units.Quantity`
        Estimate of the plasma potential in units of V.

    "I_es" : `astropy.units.Quantity`
        Estimate of the electron saturation current in units of Am^-2.

    "I_is" : `astropy.units.Quantity`
        Estimate of the ion saturation current in units of Am^-2.

    "hot_fraction" : float
        Estimate of the total hot (energetic) electron fraction.

    Notes
    -----
    This function combines the separate probe analysis functions into a single
    analysis. Results are returned as a Dictionary. On plasma densities: in an
    ideal quasi-neutral plasma all densities should be equal. However, in
    practice this will not be the case. The electron density is the poorest
    estimate due to the hard to obtain knee in the electron current. The
    density provided by OML theory is likely the best estimate as it is not
    dependent on the obtained electron temperature, given that the conditions
    for OML theory hold.

    """
    _langmuir_futurewarning()

    # Instantiate gas using the Particle class
    gas = Particle(argument=gas_argument)

    if not isinstance(probe_characteristic, Characteristic):
        raise TypeError(
            f"For 'probe_characteristic' expected type "
            f"{Characteristic.__module__ + '.' + Characteristic.__qualname__} "
            f"and got {type(probe_characteristic)}"
        )

    # Obtain the plasma and floating potentials
    V_P = get_plasma_potential(probe_characteristic)
    V_F = get_floating_potential(probe_characteristic)

    # Obtain the electron and ion saturation currents
    I_es = get_electron_saturation_current(probe_characteristic)
    I_is = get_ion_saturation_current(probe_characteristic)

    # The OML method is used to obtain an ion density without knowing the
    # electron temperature. This can then be used to obtain the ion current
    # and subsequently a better electron current fit.
    n_i_OML, fit = get_ion_density_OML(
        probe_characteristic, probe_area, gas, return_fit=True
    )

    ion_current = extrapolate_ion_current_OML(probe_characteristic, fit)

    # First electron temperature iteration
    exponential_section = extract_exponential_section(
        probe_characteristic, ion_current=ion_current
    )
    T_e, hot_fraction = get_electron_temperature(
        exponential_section, bimaxwellian=bimaxwellian, return_hot_fraction=True
    )

    # Second electron temperature iteration, using an electron temperature-
    # adjusted exponential section
    exponential_section = extract_exponential_section(
        probe_characteristic, T_e=T_e, ion_current=ion_current
    )
    T_e, hot_fraction, fit = get_electron_temperature(
        exponential_section,
        bimaxwellian=bimaxwellian,
        visualize=plot_electron_fit,
        return_fit=True,
        return_hot_fraction=True,
    )

    # Extrapolate the fit of the exponential section to obtain the full
    # electron current. This has no use in the analysis except for
    # visualization.
    electron_current = extrapolate_electron_current(
        probe_characteristic, fit, bimaxwellian=bimaxwellian
    )

    # Using a good estimate of electron temperature, obtain the ion and
    # electron densities from the saturation currents.
    n_i = get_ion_density_LM(
        I_is, reduce_bimaxwellian_temperature(T_e, hot_fraction), probe_area, gas.mass
    )
    n_e = get_electron_density_LM(
        I_es, reduce_bimaxwellian_temperature(T_e, hot_fraction), probe_area
    )

    if visualize:  # coverage: ignore
        try:
            import matplotlib.pyplot as plt
        except (ImportError, ModuleNotFoundError) as e:
            from plasmapy.optional_deps import mpl_import_error

            raise mpl_import_error from e

        with quantity_support():
            fig, (ax1, ax2) = plt.subplots(2, 1)
            ax1.plot(
                probe_characteristic.bias,
                probe_characteristic.current,
                marker=".",
                color="k",
                linestyle="",
                label="Probe current",
            )
            ax1.set_title("Probe characteristic")
            ax2.set_ylim(probe_characteristic.get_padded_limit(0.1))

            ax2.plot(
                probe_characteristic.bias,
                np.abs(probe_characteristic.current),
                marker=".",
                color="k",
                linestyle="",
                label="Probe current",
            )
            ax2.set_title("Logarithmic")
            ax2.set_ylim(probe_characteristic.get_padded_limit(0.1, log=True))

            ax1.axvline(x=V_P.value, color="gray", linestyle="--")
            ax1.axhline(y=I_es.value, color="grey", linestyle="--")
            ax1.axvline(x=V_F.value, color="k", linestyle="--")
            ax1.axhline(y=I_is.value, color="r", linestyle="--")
            ax1.plot(ion_current.bias, ion_current.current, c="y", label="Ion current")
            ax1.plot(
                electron_current.bias,
                electron_current.current,
                c="c",
                label="Electron current",
            )
            tot_current = ion_current + electron_current
            ax1.plot(tot_current.bias, tot_current.current, c="g")

            ax2.axvline(x=V_P.value, color="gray", linestyle="--")
            ax2.axhline(y=I_es.value, color="grey", linestyle="--")
            ax2.axvline(x=V_F.value, color="k", linestyle="--")
            ax2.axhline(y=np.abs(I_is.value), color="r", linestyle="--")
            ax2.plot(
                ion_current.bias,
                np.abs(ion_current.current),
                label="Ion current",
                c="y",
            )
            ax2.plot(
                electron_current.bias,
                np.abs(electron_current.current),
                label="Electron current",
                c="c",
            )
            ax2.plot(tot_current.bias, np.abs(tot_current.current), c="g")
            ax2.set_yscale("log", nonposy="clip")
            ax1.legend(loc="best")
            ax2.legend(loc="best")

            fig.tight_layout()

    # Obtain and show the EEDF. This is only useful if the characteristic data
    # has been preprocessed to be sufficiently smooth and noiseless.
    if plot_EEDF:  # coverage: ignore
        get_EEDF(probe_characteristic, visualize=True)

    # Compile the results dictionary
    results = {
        "V_P": V_P,
        "V_F": V_F,
        "I_es": I_es,
        "I_is": I_is,
        "n_e": n_e,
        "n_i": n_i,
        "T_e": T_e,
        "n_i_OML": n_i_OML,
    }

    if bimaxwellian:
        results["hot_fraction"] = hot_fraction

    return results
Esempio n. 24
0
def get_ion_density_OML(
    probe_characteristic: Characteristic,
    probe_area: u.m ** 2,
    gas,
    visualize=False,
    return_fit=False,
):
    r"""Implement the Orbital Motion Limit (OML) method of obtaining an
    estimate of the ion density.

    Parameters
    ----------
    probe_characteristic : ~plasmapy.diagnostics.langmuir.Characteristic
        The swept probe characteristic that is to be analyzed.

    probe_area : ~astropy.units.Quantity
        The area of the probe exposed to plasma in units convertible to m^2.

    gas : ~astropy.units.Quantity
        The (mean) mass of the background gas in atomic mass units.

    visualize : bool, optional
        If True a plot of the OML fit is shown. Default is False.

    return_fit: bool, optional
        If True the parameters of the fit will be returned in addition to the
        ion density. Default is False.

    Returns
    -------
    n_i_OML : ~astropy.units.Quantity
        Estimated ion density in m^-3.

    Notes
    -----
    The method implemented in this function holds for cylindrical probes in a
    cold ion plasma, ie. :math:T_i=0` eV. With OML theory an expression is found
    for the ion current as function of probe bias independent of the electron
    temperature [mott-smith.langmuir-1926]_:

    .. math::
        I_i \xrightarrow[T_i = 0]{} A_p n_i e \frac{\sqrt{2}}{\pi}
        \sqrt{\frac{e \left( V_F - V \right)}{m_i}}

    References
    ----------
    .. [mott-smith.langmuir-1926] H. M. Mott-Smith, I. Langmuir,
        Phys. Rev. 28, 727-763 (Oct. 1926)

    """

    _langmuir_futurewarning()

    if not isinstance(probe_characteristic, Characteristic):
        raise TypeError(
            f"For 'probe_characteristic' expected type "
            f"{Characteristic.__module__ + '.' + Characteristic.__qualname__} "
            f"and got {type(probe_characteristic)}"
        )

    ion_section = extract_ion_section(probe_characteristic)

    fit = np.polyfit(
        ion_section.bias.to(u.V).value, ion_section.current.to(u.mA).value ** 2, 1
    )

    poly = np.poly1d(fit)

    slope = fit[0]

    ion = Particle(argument=gas)

    n_i_OML = np.sqrt(
        -slope
        * u.mA ** 2
        / u.V
        * np.pi ** 2
        * ion.mass
        / (probe_area ** 2 * const.e ** 3 * 2)
    )

    if visualize:  # coverage: ignore
        try:
            import matplotlib.pyplot as plt
        except (ImportError, ModuleNotFoundError) as e:
            from plasmapy.optional_deps import mpl_import_error

            raise mpl_import_error from e

        with quantity_support():
            plt.figure()
            plt.scatter(
                ion_section.bias.to(u.V),
                ion_section.current.to(u.mA) ** 2,
                color="k",
                marker=".",
            )
            plt.plot(
                ion_section.bias.to(u.V), poly(ion_section.bias.to(u.V).value), c="g"
            )
            plt.title("OML fit")
            plt.tight_layout()

    if return_fit:
        return n_i_OML.to(u.m ** -3), fit

    return n_i_OML.to(u.m ** -3)
Esempio n. 25
0
class TestHollweg:
    _kwargs_single_valued = {
        "k": 0.01 * u.rad / u.m,
        "theta": 88 * u.deg,
        "n_i": 5 * u.cm**-3,
        "B": 2.2e-8 * u.T,
        "T_e": 1.6e6 * u.K,
        "T_i": 4.0e5 * u.K,
        "ion": Particle("p+"),
    }

    _kwargs_hollweg1999 = {
        "theta": 90 * u.deg,
        "n_i": 5 * u.cm**-3,
        "T_e": 1.6e6 * u.K,
        "T_i": 4.0e5 * u.K,
        "ion": Particle("p+"),
    }

    @pytest.mark.parametrize(
        "kwargs, _error",
        [
            ({
                **_kwargs_single_valued, "B": "wrong type"
            }, TypeError),
            ({
                **_kwargs_single_valued, "B": [8e-9, 8.5e-9] * u.T
            }, ValueError),
            ({
                **_kwargs_single_valued, "B": -1 * u.T
            }, ValueError),
            ({
                **_kwargs_single_valued, "B": 5 * u.m
            }, u.UnitTypeError),
            ({
                **_kwargs_single_valued, "ion": {
                    "not": "a particle"
                }
            }, TypeError),
            ({
                **_kwargs_single_valued, "ion": "e-"
            }, ValueError),
            ({
                **_kwargs_single_valued, "ion": "He",
                "z_mean": "wrong type"
            }, TypeError),
            ({
                **_kwargs_single_valued, "k": np.ones((3, 2)) * u.rad / u.m
            }, ValueError),
            ({
                **_kwargs_single_valued, "k": 0 * u.rad / u.m
            }, ValueError),
            ({
                **_kwargs_single_valued, "k": -1.0 * u.rad / u.m
            }, ValueError),
            ({
                **_kwargs_single_valued, "k": 5 * u.s
            }, u.UnitTypeError),
            ({
                **_kwargs_single_valued, "n_i": "wrong type"
            }, TypeError),
            ({
                **_kwargs_single_valued, "n_i": [5e6, 6e6] * u.m**-3
            }, ValueError),
            ({
                **_kwargs_single_valued, "n_i": -5e6 * u.m**-3
            }, ValueError),
            ({
                **_kwargs_single_valued, "n_i": 2 * u.s
            }, u.UnitTypeError),
            ({
                **_kwargs_single_valued, "T_e": "wrong type"
            }, TypeError),
            ({
                **_kwargs_single_valued, "T_e": [1.4e6, 1.7e6] * u.K
            }, ValueError),
            ({
                **_kwargs_single_valued, "T_e": -10 * u.eV
            }, ValueError),
            ({
                **_kwargs_single_valued, "T_e": 2 * u.s
            }, u.UnitTypeError),
            ({
                **_kwargs_single_valued, "T_i": "wrong type"
            }, TypeError),
            ({
                **_kwargs_single_valued, "T_i": [4e5, 5e5] * u.K
            }, ValueError),
            ({
                **_kwargs_single_valued, "T_i": -1 * u.eV
            }, ValueError),
            ({
                **_kwargs_single_valued, "T_i": 2 * u.s
            }, u.UnitTypeError),
            ({
                **_kwargs_single_valued, "theta": np.ones((3, 2)) * u.deg
            }, ValueError),
            ({
                **_kwargs_single_valued, "theta": 5 * u.eV
            }, u.UnitTypeError),
            ({
                **_kwargs_single_valued, "gamma_e": "wrong type"
            }, TypeError),
            ({
                **_kwargs_single_valued, "gamma_i": "wrong type"
            }, TypeError),
        ],
    )
    def test_raises(self, kwargs, _error):
        """Test scenarios that raise an `Exception`."""
        with pytest.raises(_error):
            hollweg(**kwargs)

    @pytest.mark.parametrize(
        "kwargs, _warning",
        [
            # w/w_ci << 1 PhysicsWarning
            (
                {
                    "k": 0.01 * u.rad / u.m,
                    "theta": 88 * u.deg,
                    "n_i": 0.05 * u.cm**-3,
                    "B": 2.2e-8 * u.T,
                    "T_e": 1.6e6 * u.K,
                    "T_i": 4.0e5 * u.K,
                    "ion": Particle("p+"),
                },
                PhysicsWarning,
            ),
            # c_s/v_A << 1 PhysicsWarning
            (
                {
                    "k": 10e-8 * u.rad / u.m,
                    "theta": 88 * u.deg,
                    "n_i": 5 * u.cm**-3,
                    "B": 6.98e-8 * u.T,
                    "T_e": 1.6e6 * u.K,
                    "T_i": 4.0e5 * u.K,
                    "ion": Particle("p+"),
                },
                PhysicsWarning,
            ),
            # theta nearly perpendicular PhysicsWarning
            (
                {
                    "k": 10e-8 * u.rad / u.m,
                    "theta": 84 * u.deg,
                    "n_i": 1 * u.cm**-3,
                    "B": 6.98e-8 * u.T,
                    "T_e": 1.6e6 * u.K,
                    "T_i": 4.0e5 * u.K,
                    "ion": Particle("p+"),
                },
                PhysicsWarning,
            ),
        ],
    )
    def test_warning(self, kwargs, _warning):
        """Test scenarios that raise a `Warning`."""
        with pytest.warns(_warning):
            hollweg(**kwargs)

    @pytest.mark.parametrize(
        "kwargs, expected, desired_beta",
        [
            (  # beta = 1/20 for kx*L = 0
                {
                    **_kwargs_hollweg1999, "k": 1e-14 * u.rad / u.m,
                    "B": 6.971e-8 * u.T
                },
                1 + 0j,
                1 / 20,
            ),
            (  # beta = 1/20 for kx*L = 1
                {
                    **_kwargs_hollweg1999,
                    "k": 0.0000439223874624874 * u.rad / u.m,
                    "B": 6.971e-8 * u.T,
                },
                1.4018 + 0j,
                1 / 20,
            ),
            (  # beta = 1/2 for kx*L = 0
                {
                    **_kwargs_hollweg1999, "k": 1e-14 * u.rad / u.m,
                    "B": 2.205e-8 * u.T
                },
                1 + 0j,
                0.5,
            ),
            (  # beta = 1/2 for kx*L = 1
                {
                    **_kwargs_hollweg1999,
                    "k": 0.000013893109303101 * u.rad / u.m,
                    "B": 2.205e-8 * u.T,
                },
                1.3536 + 0j,
                0.5,
            ),
            (  # beta = 2 for kx*L = 0
                {
                    **_kwargs_hollweg1999,
                    "k": 1e-14 * u.rad / u.m,
                    "B": 1.10232e-8 * u.T,
                },
                1 + 0j,
                2,
            ),
            (  # beta = 2 for kx*L = 1
                {
                    **_kwargs_hollweg1999,
                    "k":
                    0.00000691190063354451 * u.rad / u.m,
                    "B":
                    1.10232e-8 * u.T,
                },
                1.2607 + 0j,
                2,
            ),
            (  # beta = 1/2000 for kx*L = 0
                {
                    **_kwargs_hollweg1999,
                    "k": 1e-14 * u.rad / u.m,
                    "B": 6.97178e-7 * u.T,
                },
                1 + 0j,
                1 / 2000,
            ),
            (  # beta = 1/2000 for kx*L = 1
                {
                    **_kwargs_hollweg1999,
                    "k": 0.000439273010336778 * u.rad / u.m,
                    "B": 6.97178e-7 * u.T,
                },
                0.98750 + 0j,
                1 / 2000,
            ),
        ],
    )
    def test_hollweg1999_vals(self, kwargs, expected, desired_beta):
        """
        Test calculated values based on Figure 2 of Hollweg1999
        (DOI: https://doi.org/10.1029/1998JA900132) using eqn 3 of
        Bellan 2012
        (DOI: https://doi.org/10.1029/2012JA017856).

        The WebPlotDigitizer software was used to determine the test
        parameters for k, B, and expected omega from Figure 2 of
        Hollweg1999.

        - GitHub: https://github.com/ankitrohatgi/WebPlotDigitizer
        - Web Version: https://automeris.io/WebPlotDigitizer/
        """
        # k values need to be single valued for this test to function correctly

        cs = speeds.cs_(kwargs["T_e"], kwargs["T_i"], kwargs["ion"]).value
        va = speeds.va_(kwargs["B"], kwargs["n_i"], ion=kwargs["ion"]).value

        beta = (cs / va)**2
        if not np.isclose(beta, desired_beta, atol=2e-4):
            pytest.fail(
                f"The Holweg 1999 paper requires a 'beta' value of {desired_beta:0.5f} "
                f"and the test parameters yielded {beta:.6f}.")

        kz = (np.cos(kwargs["theta"]) * kwargs["k"]).value

        w_alfven = (hollweg(**kwargs)["alfven_mode"]).value
        big_omega = np.abs(w_alfven / (kz * va))

        assert np.allclose(big_omega, expected, atol=1e-2)

    @pytest.mark.parametrize(
        "kwargs, expected",
        [
            (
                {
                    **_kwargs_single_valued,
                    "ion": Particle("He"),
                    "z_mean": 2.0,
                },
                {
                    **_kwargs_single_valued, "ion": Particle("He +2")
                },
            ),
            #
            # z_mean defaults to 1
            (
                {
                    **_kwargs_single_valued, "ion": Particle("He")
                },
                {
                    **_kwargs_single_valued, "ion": Particle("He+")
                },
            ),
        ],
    )
    def test_z_mean_override(self, kwargs, expected):
        """Test overriding behavior of kw 'z_mean'."""
        ws = hollweg(**kwargs)
        ws_expected = hollweg(**expected)

        for mode in ws:
            assert np.isclose(ws[mode],
                              ws_expected[mode],
                              atol=1e-5,
                              rtol=1.7e-4)

    @pytest.mark.parametrize(
        "kwargs, expected",
        [
            ({
                **_kwargs_single_valued
            }, {
                "shape": ()
            }),
            (
                {
                    **_kwargs_single_valued,
                    "k": [1, 2, 3] * u.rad / u.m,
                },
                {
                    "shape": (3, )
                },
            ),
            (
                {
                    **_kwargs_single_valued,
                    "k": [1, 2, 3] * u.rad / u.m,
                    "theta": [50, 77] * u.deg,
                },
                {
                    "shape": (3, 2)
                },
            ),
            (
                {
                    **_kwargs_single_valued,
                    "theta": [50, 77] * u.deg,
                },
                {
                    "shape": (2, )
                },
            ),
        ],
    )
    def test_return_structure(self, kwargs, expected):
        """Test the structure of the returned values."""
        ws = hollweg(**kwargs)

        assert isinstance(ws, dict)
        assert len({"acoustic_mode", "alfven_mode", "fast_mode"} -
                   set(ws.keys())) == 0

        for mode, val in ws.items():
            assert isinstance(val, u.Quantity)
            assert val.unit == u.rad / u.s
            assert val.shape == expected["shape"]
def alfven_dispersion_solution(
    *,
    B: u.T,
    ion: Union[str, Particle],
    k: u.rad / u.m,
    n_i: u.m**-3,
    T_e: u.K,
    T_i: u.K,
    theta: u.deg,
    gamma_e: Union[float, int] = 1,
    gamma_i: Union[float, int] = 3,
    z_mean: Union[float, int] = None,
):

    # validate argument ion
    if not isinstance(ion, Particle):
        try:
            ion = Particle(ion)
        except TypeError:
            raise TypeError(
                f"For argument 'ion' expected type {Particle} but got {type(ion)}."
            )
    if not (ion.is_ion or ion.is_category("element")):
        raise ValueError(
            "The particle passed for 'ion' must be an ion or element.")

    # validate z_mean
    if z_mean is None:
        try:
            z_mean = abs(ion.integer_charge)
        except ChargeError:
            z_mean = 1
    else:
        if not isinstance(z_mean, (int, np.integer, float, np.floating)):
            raise TypeError(
                f"Expected int or float for argument 'z_mean', but got {type(z_mean)}."
            )
        z_mean = abs(z_mean)

    # validate arguments
    for arg_name in ("B", "n_i", "T_e", "T_i"):
        val = locals()[arg_name].squeeze()
        if val.shape != ():
            raise ValueError(
                f"Argument '{arg_name}' must a single value and not an array of "
                f"shape {val.shape}.")
        locals()[arg_name] = val

    # validate arguments
    for arg_name in ("gamma_e", "gamma_i"):
        if not isinstance(locals()[arg_name],
                          (int, np.integer, float, np.floating)):
            raise TypeError(
                f"Expected int or float for argument '{arg_name}', but got "
                f"{type(locals()[arg_name])}.")

    # validate argument k
    k = k.squeeze()
    if not (k.ndim == 0 or k.ndim == 1):
        raise ValueError(
            f"Argument 'k' needs to be a single valued or 1D array astropy Quantity,"
            f" got array of shape {k.shape}.")
    if np.any(k <= 0):
        raise ValueError("Argument 'k' can not be a or have negative values.")

    # validate argument theta
    theta = theta.squeeze()
    theta = theta.to(u.radian)
    if not (theta.ndim == 0 or theta.ndim == 1):
        raise ValueError(
            f"Argument 'theta' needs to be a single valued or 1D array astropy "
            f"Quantity, got array of shape {k.shape}.")

    n_e = z_mean * n_i
    c_s = pfp.ion_sound_speed(
        T_e=T_e,
        T_i=T_i,
        ion=ion,
        n_e=n_e,
        gamma_e=gamma_e,
        gamma_i=gamma_i,
        z_mean=z_mean,
    )
    v_A = pfp.Alfven_speed(B, n_i, ion=ion, z_mean=z_mean)
    omega_ci = pfp.gyrofrequency(B=B, particle=ion, signed=False, Z=z_mean)

    #Grid/vector creation for k?

    #Parameters kz

    kz = np.cos(theta.value) * k
    kx = np.sqrt(k**2 - kz**2)

    #Parameters sigma, D, and F to simplify equation 3
    A = (kz * v_A)**2
    F = ((kx * c_s) / omega_ci)**2

    omega = np.sqrt(A * (1 + F))
    print(omega_ci)
    return omega
Esempio n. 27
0
def mass_density(
    density: (u.m**-3, u.kg / (u.m**3)),
    particle: Union[Particle, str],
    z_ratio: Optional[numbers.Real] = 1,
) -> u.kg / u.m**3:
    r"""
    Calculate the mass density from a number density.

    .. math::

        \rho = \left| \frac{Z_{s}}{Z_{particle}} \right| n_{s} m_{particle}
              = | Z_{ratio} | n_{s} m_{particle}

    where :math:`m_{particle}` is the particle mass, :math:`n_{s}` is a number
    density for plasma species :math:`s`, :math:`Z_{s}` is the charge number of
    species :math:`s`, and :math:`Z_{particle}` is the charge number of
    ``particle``.  For example, if the electron density is given for :math:`n_s`
    and ``particle`` is a doubly ionized atom, then :math:`Z_{ratio} = -1 / 2`\ .

    **Aliases:** `rho_`

    Parameters
    ----------
    density : `~astropy.units.Quantity`
        Either a particle number density (in units of m\ :sup:`-3` or
        equivalent) or a mass density (in units of kg / m\ :sup:`3` or
        equivalent).  If ``density`` is a mass density, then it will be passed
        through and returned without modification.

    particle : `~plasmapy.particles.particle_class.Particle`
        The particle for which the mass density is being calculated for.  Must
        be a `~plasmapy.particles.particle_class.Particle` or a value convertible to
        a `~plasmapy.particles.particle_class.Particle` (e.g., ``'p'`` for protons,
        ``'D+'`` for deuterium, or ``'He-4 +1'`` for singly ionized helium-4).

    z_ratio : `int`, `float`, optional
        The ratio of the charge numbers corresponding to the plasma species
        represented by ``density`` and the ``particle``.  For example, if the
        given ``density`` is and electron density and ``particle`` is doubly
        ionized ``He``, then ``z_ratio = -0.5``.  Default is ``1``.

    Raises
    ------
    `~astropy.units.UnitTypeError`
        If the ``density`` does not have units equivalent to a number density
        or mass density.

    `TypeError`
        If ``density`` is not of type `~astropy.units.Quantity`, or convertible.

    `TypeError`
        If ``particle`` is not of type or convertible to
        `~plasmapy.particles.particle_class.Particle`.

    `TypeError`
        If ``z_ratio`` is not of type `int` or `float`.

    `ValueError`
        If ``density`` is negative.

    Returns
    -------
    `~astropy.units.Quantity`
        The mass density for the plasma species represented by ``particle``.

    Examples
    --------
    >>> import astropy.units as u
    >>> mass_density(1 * u.m ** -3, 'p')
    <Quantity 1.67262...e-27 kg / m3>
    >>> mass_density(4 * u.m ** -3, 'D+')
    <Quantity 1.33743...e-26 kg / m3>
    >>> mass_density(2.e12 * u.cm ** -3, 'He')
    <Quantity 1.32929...e-08 kg / m3>
    >>> mass_density(2.e12 * u.cm ** -3, 'He', z_ratio=0.5)
    <Quantity 6.64647...e-09 kg / m3>
    >>> mass_density(1.0 * u.g * u.m ** -3, "")
    <Quantity 0.001 kg / m3>
    """
    if density.unit.is_equivalent(u.kg / u.m**3):
        return density

    if not isinstance(particle, Particle):
        try:
            particle = Particle(particle)
        except TypeError as e:
            raise TypeError(
                f"If passing a number density, you must pass a plasmapy Particle "
                f"(not type {type(particle)}) to calculate the mass density!"
            ) from e

    if not isinstance(z_ratio, (float, np.floating, int, np.integer)):
        raise TypeError(
            f"Expected type int or float for keyword z_ratio, got type {type(z_ratio)}."
        )

    return abs(z_ratio) * density * particle.mass
Esempio n. 28
0
class TestTwoFluidDispersionSolution:
    _kwargs_single_valued = {
        "B": 8.3e-9 * u.T,
        "ion": "p+",
        "k": 0.0001 * u.rad / u.m,
        "n_i": 5.0e6 * u.m ** -3,
        "T_e": 1.6e6 * u.K,
        "T_i": 4.0e5 * u.K,
        "theta": 45 * u.deg,
    }
    _kwargs_bellan2012 = {
        "B": 400e-4 * u.T,
        "ion": Particle("He+"),
        "n_i": 6.358e19 * u.m ** -3,
        "T_e": 20 * u.eV,
        "T_i": 10 * u.eV,
        "k": (2 * np.pi * u.rad) / (0.56547 * u.m),
    }

    @pytest.mark.parametrize(
        "kwargs, _error",
        [
            ({**_kwargs_single_valued, "B": "wrong type"}, TypeError),
            ({**_kwargs_single_valued, "B": [8e-9, 8.5e-9] * u.T}, ValueError),
            ({**_kwargs_single_valued, "B": -1 * u.T}, ValueError),
            ({**_kwargs_single_valued, "B": 5 * u.m}, u.UnitTypeError),
            ({**_kwargs_single_valued, "ion": {"not": "a particle"}}, TypeError),
            ({**_kwargs_single_valued, "ion": "e-"}, ValueError),
            ({**_kwargs_single_valued, "ion": "He", "z_mean": "wrong type"}, TypeError),
            ({**_kwargs_single_valued, "k": np.ones((3, 2)) * u.rad / u.m}, ValueError),
            ({**_kwargs_single_valued, "k": 0 * u.rad / u.m}, ValueError),
            ({**_kwargs_single_valued, "k": -1.0 * u.rad / u.m}, ValueError),
            ({**_kwargs_single_valued, "k": 5 * u.s}, u.UnitTypeError),
            ({**_kwargs_single_valued, "n_i": "wrong type"}, TypeError),
            ({**_kwargs_single_valued, "n_i": [5e6, 6e6] * u.m ** -3}, ValueError),
            ({**_kwargs_single_valued, "n_i": -5e6 * u.m ** -3}, ValueError),
            ({**_kwargs_single_valued, "n_i": 2 * u.s}, u.UnitTypeError),
            ({**_kwargs_single_valued, "T_e": "wrong type"}, TypeError),
            ({**_kwargs_single_valued, "T_e": [1.4e6, 1.7e6] * u.K}, ValueError),
            ({**_kwargs_single_valued, "T_e": -10 * u.eV}, ValueError),
            ({**_kwargs_single_valued, "T_e": 2 * u.s}, u.UnitTypeError),
            ({**_kwargs_single_valued, "T_i": "wrong type"}, TypeError),
            ({**_kwargs_single_valued, "T_i": [4e5, 5e5] * u.K}, ValueError),
            ({**_kwargs_single_valued, "T_i": -1 * u.eV}, ValueError),
            ({**_kwargs_single_valued, "T_i": 2 * u.s}, u.UnitTypeError),
            ({**_kwargs_single_valued, "theta": np.ones((3, 2)) * u.deg}, ValueError),
            ({**_kwargs_single_valued, "theta": 5 * u.eV}, u.UnitTypeError),
            ({**_kwargs_single_valued, "gamma_e": "wrong type"}, TypeError),
            ({**_kwargs_single_valued, "gamma_i": "wrong type"}, TypeError),
        ],
    )
    def test_raises(self, kwargs, _error):
        """Test scenarios that raise an `Exception`."""
        with pytest.raises(_error):
            two_fluid_dispersion_solution(**kwargs)

    @pytest.mark.parametrize(
        "kwargs, _warning",
        [
            # violates the low-frequency assumption (w/kc << 1)
            (
                {
                    "B": 8.3e-7 * u.T,
                    "ion": "p+",
                    "k": 0.0001 * u.rad / u.m,
                    "n_i": 3.0e6 * u.m ** -3,
                    "T_e": 1.6e6 * u.K,
                    "T_i": 4.0e5 * u.K,
                    "theta": 5 * u.deg,
                },
                PhysicsWarning,
            ),
        ],
    )
    def test_warns(self, kwargs, _warning):
        """Test scenarios the issue a `Warning`."""
        with pytest.warns(_warning):
            two_fluid_dispersion_solution(**kwargs)

    @pytest.mark.parametrize(
        "kwargs, expected",
        [
            (
                {**_kwargs_bellan2012, "theta": 0 * u.deg},
                {
                    "fast_mode": 1.8631944,
                    "alfven_mode": 0.5366538,
                    "acoustic_mode": 0.4000832,
                },
            ),
            (
                {**_kwargs_bellan2012, "theta": 90 * u.deg},
                {"fast_mode": 1.4000284, "alfven_mode": 0.0, "acoustic_mode": 0.0},
            ),
        ],
    )
    def test_on_bellan2012_vals(self, kwargs, expected):
        """
        Test calculated values based on Figure 1 of Bellan 2012
        (DOI: https://agupubs.onlinelibrary.wiley.com/doi/10.1029/2012JA017856).
        """
        # theta and k values need to be single valued for this test to function
        # correctly

        cs = pfp.cs_(kwargs["T_e"], kwargs["T_i"], kwargs["ion"])
        va = pfp.va_(kwargs["B"], kwargs["n_i"], ion=kwargs["ion"])
        wci = pfp.wc_(kwargs["B"], kwargs["ion"])

        beta = (cs / va).value ** 2
        if not np.isclose(beta, 0.4, atol=1e-4):
            pytest.fail(
                f"The Bellan 2012 paper requires a 'beta' value of 0.4 and the test "
                f"parameters yielded {beta:.6f}."
            )

        Lambda = (kwargs["k"] * va / wci).value ** 2
        if not np.isclose(Lambda, 0.4, atol=1e-4):
            pytest.fail(
                f"The Bellan 2012 paper requires a 'Lambda' value of 0.4 and the test "
                f"parameters yielded {Lambda:.6f}."
            )

        ws = two_fluid_dispersion_solution(**kwargs)
        for mode, val in ws.items():
            norm = (np.absolute(val) / (kwargs["k"] * va)).value ** 2
            assert np.isclose(norm, expected[mode])

    @pytest.mark.parametrize(
        "kwargs, expected",
        [
            (
                {
                    **_kwargs_bellan2012,
                    "ion": Particle("He"),
                    "z_mean": 2.0,
                    "theta": 0 * u.deg,
                },
                {**_kwargs_bellan2012, "ion": Particle("He +2"), "theta": 0 * u.deg},
            ),
            #
            # z_mean defaults to 1
            (
                {**_kwargs_bellan2012, "ion": Particle("He"), "theta": 0 * u.deg},
                {**_kwargs_bellan2012, "ion": Particle("He+"), "theta": 0 * u.deg},
            ),
        ],
    )
    def test_z_mean_override(self, kwargs, expected):
        """Test overriding behavior of kw 'z_mean'."""
        ws = two_fluid_dispersion_solution(**kwargs)
        ws_expected = two_fluid_dispersion_solution(**expected)

        for mode in ws:
            assert np.isclose(ws[mode], ws_expected[mode], atol=0, rtol=1.7e-4)

    @pytest.mark.parametrize(
        "kwargs, expected",
        [
            ({**_kwargs_bellan2012, "theta": 0 * u.deg}, {"shape": ()}),
            (
                {
                    **_kwargs_bellan2012,
                    "theta": 0 * u.deg,
                    "k": [1, 2, 3] * u.rad / u.m,
                },
                {"shape": (3,)},
            ),
            (
                {
                    **_kwargs_bellan2012,
                    "theta": [10, 20, 30, 40, 50] * u.deg,
                    "k": [1, 2, 3] * u.rad / u.m,
                },
                {"shape": (3, 5)},
            ),
            (
                {**_kwargs_bellan2012, "theta": [10, 20, 30, 40, 50] * u.deg},
                {"shape": (5,)},
            ),
        ],
    )
    def test_return_structure(self, kwargs, expected):
        """Test the structure of the returned values."""
        ws = two_fluid_dispersion_solution(**kwargs)

        assert isinstance(ws, dict)
        assert len({"acoustic_mode", "alfven_mode", "fast_mode"} - set(ws.keys())) == 0

        for mode, val in ws.items():
            assert isinstance(val, u.Quantity)
            assert val.unit == u.rad / u.s
            assert val.shape == expected["shape"]
Esempio n. 29
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,\omega) = \sum_e \frac{2\pi}{k}
        \bigg |1 - \frac{\chi_e}{\epsilon} \bigg |^2
        f_{e0,e} \bigg (\frac{\omega}{k} \bigg ) +
        \sum_i \frac{2\pi Z_i}{k}
        \bigg |\frac{\chi_e}{\epsilon} \bigg |^2 f_{i0,i}
        \bigg ( \frac{\omega}{k} \bigg )

    where :math:`\chi_e` is the electron component susceptibility of the
    plasma and :math:`\epsilon = 1 + \sum_e \chi_e + \sum_i \chi_i` is the total
    plasma dielectric  function (with :math:`\chi_i` being the ion component
    of the susceptibility), :math:`Z_i` is the charge of each ion, :math:`k`
    is the scattering wavenumber, :math:`\omega` 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 `Sheffield`_.

    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^-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`, shape (Ni, ), optional
        A list or single instance of `~plasmapy.particles.Particle`, or strings
        convertible to `~plasmapy.particles.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 degree 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 "Plasma Scattering of Electromagnetic Radiation" by
    Sheffield et al. `ISBN 978\\-0123748775`_. This code is a modified version
    of the program described therein.

    For a concise summary of the relevant physics, see Chapter 5 of Derek
    Schaeffer's thesis, DOI: `10.5281/zenodo.3766933`_.

    .. _`ISBN 978\\-0123748775`: https://www.sciencedirect.com/book/9780123748775/plasma-scattering-of-electromagnetic-radiation
    .. _`10.5281/zenodo.3766933`: https://doi.org/10.5281/zenodo.3766933
    .. _`Sheffield`: https://doi.org/10.1016/B978-0-12-374877-5.00003-8
    """
    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 electon 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 "
                         f"{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 "
                         f"{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.integer_charge * 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))
    # Normal vector along k
    k_vec = (scatter_vec - probe_vec) * u.dimensionless_unscaled

    # 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, fract in enumerate(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
Esempio n. 30
0
        {
            "should_warn": False
        },
        (5 * u.m / u.s, UserWarning),
        MissingWarningError,
    ],
    [return_arg, u.kg / u.K, {}, u.kg / u.K, None],
    [return_arg, u.kg / u.K, {}, u.kg / u.N, u.UnitsError],
    [return_arg, u.kg, {}, u.g, u.UnitsError],
    [return_arg, u.C, {
        "should_warn": True
    }, (u.C, UserWarning), None],
    [adams_number, 1, {
        "x": 1
    }, u.pc, u.UnitsError],
    [return_arg, Particle("p+"), {},
     Particle("proton"), None],
    [return_arg,
     Particle("e+"), {},
     Particle("e-"), UnexpectedResultError],
    [return_arg, Particle("mu+"), {}, type, InconsistentTypeError],
    [return_arg, (2, ), {}, IOError, MissingExceptionError],
]


@pytest.mark.parametrize("f, args, kwargs, expected, whaterror",
                         f_args_kwargs_expected_whaterror)
def test_run_test(f, args, kwargs, expected, whaterror):
    """
    Test the behavior of the test helper function.