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
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)
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)
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"])
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
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) ]
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)
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)
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)
def __getitem__(self, value) -> State: """Return information for a single ionization level.""" if isinstance(value, slice): raise TypeError("IonizationState instances cannot be sliced.") if isinstance(value, Integral) and 0 <= value <= self.atomic_number: result = State( value, self.ionic_fractions[value], self.ionic_symbols[value], self.number_densities[value], ) else: if not isinstance(value, Particle): try: value = Particle(value) except InvalidParticleError as exc: raise InvalidParticleError( f"{value} is not a valid integer charge or " f"particle.") from exc same_element = value.element == self.element same_isotope = value.isotope == self.isotope has_charge_info = value.is_category( any_of=["charged", "uncharged"]) if same_element and same_isotope and has_charge_info: Z = value.integer_charge result = State( Z, self.ionic_fractions[Z], self.ionic_symbols[Z], self.number_densities[Z], ) else: if not same_element or not same_isotope: raise AtomicError("Inconsistent element or isotope.") elif not has_charge_info: raise ChargeError("No integer charge provided.") return result
def 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)
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}.")
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
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
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}." )
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 = ""
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}. " )
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 = ""
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
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)
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
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)
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
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
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"]
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
{ "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.