def test_upper_hybrid_frequency(): r"""Test the upper_hybrid_frequency function in frequencies.py.""" omega_uh = upper_hybrid_frequency(B, n_e=n_e) omega_uh_hz = upper_hybrid_frequency(B, n_e=n_e, to_hz=True) omega_ce = gyrofrequency(B, "e-") omega_pe = plasma_frequency(n=n_e, particle="e-") assert omega_ce.unit.is_equivalent(u.rad / u.s) assert omega_pe.unit.is_equivalent(u.rad / u.s) assert omega_uh.unit.is_equivalent(u.rad / u.s) assert omega_uh_hz.unit.is_equivalent(u.Hz) left_hand_side = omega_uh**2 right_hand_side = omega_ce**2 + omega_pe**2 assert np.isclose(left_hand_side.value, right_hand_side.value) assert np.isclose(omega_uh_hz.value, 69385868857.90918) with pytest.raises(ValueError): upper_hybrid_frequency(5 * u.T, n_e=-1 * u.m**-3) with pytest.warns(u.UnitsWarning): assert upper_hybrid_frequency(1.2, 1.3) == upper_hybrid_frequency( 1.2 * u.T, 1.3 * u.m**-3) with pytest.warns(u.UnitsWarning): assert upper_hybrid_frequency(1.4 * u.T, 1.3) == upper_hybrid_frequency( 1.4, 1.3 * u.m**-3) assert_can_handle_nparray(upper_hybrid_frequency)
def test_lower_hybrid_frequency(): r"""Test the lower_hybrid_frequency function in frequencies.py.""" ion = "He-4 1+" omega_ci = gyrofrequency(B, particle=ion) omega_pi = plasma_frequency(n=n_i, particle=ion) omega_ce = gyrofrequency(B, "e-") omega_lh = lower_hybrid_frequency(B, n_i=n_i, ion=ion) omega_lh_hz = lower_hybrid_frequency(B, n_i=n_i, ion=ion, to_hz=True) assert omega_ci.unit.is_equivalent(u.rad / u.s) assert omega_pi.unit.is_equivalent(u.rad / u.s) assert omega_ce.unit.is_equivalent(u.rad / u.s) assert omega_lh.unit.is_equivalent(u.rad / u.s) left_hand_side = omega_lh**-2 right_hand_side = (1 / (omega_ci**2 + omega_pi**2) + omega_ci**-1 * omega_ce**-1) assert np.isclose(left_hand_side.value, right_hand_side.value) assert np.isclose(omega_lh_hz.value, 299878691.3223296) with pytest.raises(ValueError): lower_hybrid_frequency(0.2 * u.T, n_i=5e19 * u.m**-3, ion="asdfasd") with pytest.raises(ValueError): lower_hybrid_frequency(0.2 * u.T, n_i=-5e19 * u.m**-3, ion="asdfasd") with pytest.raises(ValueError): lower_hybrid_frequency(np.nan * u.T, n_i=-5e19 * u.m**-3, ion="asdfasd") with pytest.warns(u.UnitsWarning): assert lower_hybrid_frequency(1.3, 1e19, "p+") == lower_hybrid_frequency( 1.3 * u.T, 1e19 * u.m**-3, "p+") assert_can_handle_nparray(lower_hybrid_frequency)
def test_proton_electron_plasma(self): """ Test proton-electron plasma against the (approximate) analytical formulas """ B = 1 * u.T n = [1, 1] * 1 / u.m ** 3 omega = 1 * u.rad / u.s omega_ce = gyrofrequency(B, particle="e", signed=True) omega_pe = plasma_frequency(n[0], particle="e") omega_cp = abs(omega_ce) / 1860 omega_pp = omega_pe / 43 S_analytical = ( 1 - omega_pe ** 2 / (omega ** 2 - omega_ce ** 2) - omega_pp ** 2 / (omega ** 2 - omega_cp ** 2) ) D_analytical = +omega_ce / omega * omega_pe ** 2 / ( omega ** 2 - omega_ce ** 2 ) + omega_cp / omega * omega_pp ** 2 / (omega ** 2 - omega_cp ** 2) P_analytical = 1 - (omega_pe ** 2 + omega_pp ** 2) / omega ** 2 species = ["e", "p"] S, D, P = tuple_result = cold_plasma_permittivity_SDP(B, species, n, omega) assert tuple_result.sum is S assert tuple_result.difference is D assert tuple_result.plasma is P assert isinstance(tuple_result, StixTensorElements) assert np.isclose(S, S_analytical) assert np.isclose(D, D_analytical) assert np.isclose(P, P_analytical) L, R, P = rotating_tuple_result = cold_plasma_permittivity_LRP( B, species, n, omega ) assert rotating_tuple_result.left is L assert rotating_tuple_result.right is R assert rotating_tuple_result.plasma is P assert isinstance(rotating_tuple_result, RotatingTensorElements)
def gyroradius( B: u.T, particle: Particle, *, Vperp: u.m / u.s = np.nan * u.m / u.s, T_i: u.K = None, T: u.K = None, ) -> u.m: r"""Return the particle gyroradius. **Aliases:** `rc_`, `rhoc_` Parameters ---------- B : `~astropy.units.Quantity` The magnetic field magnitude in units convertible to tesla. particle : `~plasmapy.particles.particle_class.Particle` Representation of the particle species (e.g., ``'p'`` for protons, ``'D+'`` for deuterium, or ``'He-4 +1'`` for singly ionized helium-4). If no charge state information is provided, then the particles are assumed to be singly charged. Vperp : `~astropy.units.Quantity`, optional, keyword-only The component of particle velocity that is perpendicular to the magnetic field in units convertible to meters per second. T : `~astropy.units.Quantity`, optional, keyword-only The particle temperature in units convertible to kelvin. T_i : `~astropy.units.Quantity`, optional, keyword-only The particle temperature in units convertible to kelvin. Note: Deprecated. Use ``T`` instead. Returns ------- r_Li : `~astropy.units.Quantity` The particle gyroradius in units of meters. This `~astropy.units.Quantity` will be based on either the perpendicular component of particle velocity as inputted, or the most probable speed for a particle within a Maxwellian distribution for the particle temperature. Raises ------ `TypeError` The arguments are of an incorrect type. `~astropy.units.UnitConversionError` The arguments do not have appropriate units. `ValueError` If any argument contains invalid values. Warns ----- : `~astropy.units.UnitsWarning` If units are not provided, SI units are assumed. Notes ----- One but not both of ``Vperp`` and ``T`` must be inputted. If any of ``B``, ``Vperp``, or ``T`` is a number rather than a `~astropy.units.Quantity`, then SI units will be assumed and a warning will be raised. The particle gyroradius is also known as the particle Larmor radius and is given by .. math:: r_{Li} = \frac{V_{\perp}}{ω_{ci}} where :math:`V_⟂` is the component of particle velocity that is perpendicular to the magnetic field and :math:`ω_{ci}` is the particle gyrofrequency. If a temperature is provided, then :math:`V_⟂` will be the most probable thermal velocity of a particle at that temperature. Examples -------- >>> from astropy import units as u >>> gyroradius(0.2*u.T, particle='p+', T=1e5*u.K) <Quantity 0.002120... m> >>> gyroradius(0.2*u.T, particle='p+', T=1e5*u.K) <Quantity 0.002120... m> >>> gyroradius(5*u.uG, particle='alpha', T=1*u.eV) <Quantity 288002.38... m> >>> gyroradius(400*u.G, particle='Fe+++', Vperp=1e7*u.m/u.s) <Quantity 48.23129... m> >>> gyroradius(B=0.01*u.T, particle='e-', T=1e6*u.K) <Quantity 0.003130... m> >>> gyroradius(0.01*u.T, 'e-', Vperp=1e6*u.m/u.s) <Quantity 0.000568... m> >>> gyroradius(0.2*u.T, 'e-', T=1e5*u.K) <Quantity 4.94949...e-05 m> >>> gyroradius(5*u.uG, 'e-', T=1*u.eV) <Quantity 6744.25... m> >>> gyroradius(400*u.G, 'e-', Vperp=1e7*u.m/u.s) <Quantity 0.001421... m> """ # Backwards Compatibility and Deprecation check for keyword T_i if T_i is not None: warnings.warn( "Keyword T_i is deprecated, use T instead.", PlasmaPyFutureWarning, ) if T is None: T = T_i else: raise ValueError( "Keywords T_i and T are both given. T_i is deprecated, " "please use T only." ) if T is None: T = np.nan * u.K isfinite_T = np.isfinite(T) isfinite_Vperp = np.isfinite(Vperp) # check 1: ensure either Vperp or T invalid, keeping in mind that # the underlying values of the astropy quantity may be numpy arrays if np.any(np.logical_and(isfinite_Vperp, isfinite_T)): raise ValueError( "Must give Vperp or T, but not both, as arguments to gyroradius" ) # check 2: get Vperp as the thermal speed if is not already a valid input if np.isscalar(Vperp.value) and np.isscalar( T.value ): # both T and Vperp are scalars # we know exactly one of them is nan from check 1 if isfinite_T: # T is valid, so use it to determine Vperp Vperp = speeds.thermal_speed(T, particle=particle) # else: Vperp is already valid, do nothing elif np.isscalar(Vperp.value): # only T is an array # this means either Vperp must be nan, or T must be an array of all nan, # or else we couldn't have gotten through check 1 if isfinite_Vperp: # Vperp is valid, T is a vector that is all nan # uh... Vperp = np.repeat(Vperp, len(T)) else: # normal case where Vperp is scalar nan and T is valid array Vperp = speeds.thermal_speed(T, particle=particle) elif np.isscalar(T.value): # only Vperp is an array # this means either T must be nan, or V_perp must be an array of all nan, # or else we couldn't have gotten through check 1 if isfinite_T: # T is valid, V_perp is an array of all nan # uh... Vperp = speeds.thermal_speed(np.repeat(T, len(Vperp)), particle=particle) # else: normal case where T is scalar nan and Vperp is already a valid # array so, do nothing else: # both T and Vperp are arrays # we know all the elementwise combinations have one nan and one finite, # due to check 1 use the valid Vperps, and replace the others with those # calculated from T Vperp = Vperp.copy() # avoid changing Vperp's value outside function Vperp[isfinite_T] = speeds.thermal_speed(T[isfinite_T], particle=particle) omega_ci = frequencies.gyrofrequency(B, particle) return np.abs(Vperp) / omega_ci
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
def hollweg( *, 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""" Calculate the two fluid dispersion relation presented by :cite:t:`hollweg:1999`, and discussed by :cite:t:`bellan:2012`. This is a numberical solver of equation 3 in :cite:t:`bellan:2012`. See the **Notes** section below for additional details. 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 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 x 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 :math:`\omega / \omega_{\rm ci} > 0.1`, violation of the low-frequency assumption. : `~plasmapy.utils.exceptions.PhysicsWarning` When :math:`c_{\rm s} / v_{\rm A} > 0.1`, violation of low-β. : `~plasmapy.utils.exceptions.PhysicsWarning` When :math:`|θ - π/2| > 0.1`, violation of quasi-perpendicular propagation. Notes ----- The dispersion relation presented in :cite:t:`hollweg:1999` (equation 3 in :cite:t:`bellan:2012`) is: .. math:: \left( \frac{\omega^2}{k_{\rm z}^2 v_{\rm A}^2} - 1 \right) & \left[ \omega^2 \left( \omega^2 - k^2 v_{\rm A}^2 \right) - \beta k^2 v_{\rm A}^2 \left( \omega^2 - k_{\rm z}^2 v_{\rm A}^2 \right) \right] \\ &= \omega^2 \left(\omega^2 - k^2 v_{\rm A}^2 \right) k_{\rm x}^2 \left( \frac{c_{\rm s}^2}{\omega_{\rm ci}^2} - \frac{c^2}{\omega_{\rm pe}^2} \frac{\omega^2}{k_{\rm z}^2v_{\rm A}^2} \right) where .. math:: \mathbf{B_o} &= B_{o} \mathbf{\hat{z}} \\ \cos \theta &= \frac{k_z}{k} \\ \mathbf{k} &= k_{\rm x} \hat{x} + k_{\rm z} \hat{z} :math:`\omega` is the wave frequency, :math:`k` is the wavenumber, :math:`v_{\rm A}` is the Alfvén velocity, :math:`c_{\rm s}` is the sound speed, :math:`\omega_{\rm ci}` is the ion gyrofrequency, and :math:`\omega_{\rm pe}` is the electron plasma frequency. In the derivation of this relation Hollweg assumed low-frequency waves :math:`\omega / \omega_{\rm ci} \ll 1`, no D.C. electric field :math:`\mathbf{E_o}=0`, and quasi-neutrality. :cite:t:`hollweg:1999` asserts this expression is valid for arbitrary :math:`c_{\rm s} / v_{\rm A}` (β) and :math:`k_{\rm z} / k` (θ). Contrarily, :cite:t:`bellan:2012` states in §1.7 that due to the inconsistent retention of the :math:`\omega / \omega_{\rm ci} \ll 1` terms the expression can only be valid if both :math:`c_{\rm s} \ll v_{\rm A}` (low-β) and the wave propgation is nearly perpendicular to the magnetic field. This routine solves for ω for given :math:`k` values by numerically solving for the roots of the above expression. Examples -------- >>> from astropy import units as u >>> from plasmapy.dispersion.numerical import hollweg_ >>> inputs = { ... "k": np.logspace(-7, -2, 2) * 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+"), ... } >>> omegas = hollweg(**inputs) >>> omegas {'fast_mode': <Quantity [2.62911663e-02+0.j, 2.27876968e+03+0.j] rad / s>, 'alfven_mode': <Quantity [7.48765909e-04+0.j, 2.13800404e+03+0.j] rad / s>, 'acoustic_mode': <Quantity [0.00043295+0.j, 0.07358991+0.j] 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 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.charge_number) 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 be single valued 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 single valued or a 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 {theta.shape}.") # Single k value case if np.isscalar(k.value): k = np.array([k.value]) * u.rad / u.m # Calc needed plasma parameters with warnings.catch_warnings(): warnings.simplefilter("ignore", category=PhysicsWarning) n_e = z_mean * n_i 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, ).value v_A = Alfven_speed(B, n_i, ion=ion, z_mean=z_mean).value omega_ci = gyrofrequency(B=B, particle=ion, signed=False, Z=z_mean).value omega_pe = plasma_frequency(n=n_e, particle="e-").value cs_vA = c_s / v_A thetav, kv = np.meshgrid(theta.value, k.value) # Parameters kx and kz kz = np.cos(thetav) * kv kx = np.sin(thetav) * kv # Define helpful parameters beta = (c_s / v_A)**2 alpha_A = (kv * v_A)**2 alpha_s = (kv * c_s)**2 # == alpha_A * beta sigma = (kz * v_A)**2 D = (c_s / omega_ci)**2 F = (c_si_unitless / omega_pe)**2 # Polynomial coefficients: c3*x^3 + c2*x^2 + c1*x + c0 = 0 c3 = F * kx**2 + 1 c2 = -alpha_A * (1 + beta + F * kx**2) - sigma * (1 + D * kx**2) c1 = sigma * alpha_A * (1 + 2 * beta + D * kx**2) c0 = -alpha_s * sigma**2 # Find roots to polynomial coefficients = np.array([c3, c2, c1, c0], ndmin=3) nroots = coefficients.shape[0] - 1 # 3 nks = coefficients.shape[1] nthetas = coefficients.shape[2] roots = np.empty((nroots, nks, nthetas), dtype=np.complex128) for ii in range(nks): for jj in range(nthetas): roots[:, ii, jj] = np.roots(coefficients[:, ii, jj]) roots = np.sqrt(roots) roots = np.sort(roots, axis=0) # Warn about NOT low-beta if c_s / v_A > 0.1: warnings.warn( f"This solver is valid in the low-beta regime, " f"c_s/v_A << 1 according to Bellan, 2012, Sec. 1.7 " f"(see documentation for DOI). A c_s/v_A value of {cs_vA:.2f} " f"was calculated which may affect the validity of the solution.", PhysicsWarning, ) # Warn about theta not nearly perpendicular theta_diff_max = np.amax(np.abs(thetav - np.pi / 2)) if theta_diff_max > 0.1: warnings.warn( f"This solver is valid in the regime where propagation is " f"nearly perpendicular to B according to Bellan, 2012, Sec. 1.7 " f"(see documentation for DOI). A |theta - pi/2| value of " f"{theta_diff_max:.2f} was calculated which may affect the " f"validity of the solution.", PhysicsWarning, ) # dispersion relation is only valid in the regime w << w_ci w_max = np.max(roots) w_wci_max = w_max / omega_ci if w_wci_max > 0.1: warnings.warn( f"This solver is valid in the regime w/w_ci << 1. A w " f"value of {w_max:.2f} and a w/w_ci value of " f"{w_wci_max:.2f} were calculated which may affect the " f"validity of the solution.", PhysicsWarning, ) omegas = { "fast_mode": roots[2, :].squeeze() * u.rad / u.s, "alfven_mode": roots[1, :].squeeze() * u.rad / u.s, "acoustic_mode": roots[0, :].squeeze() * u.rad / u.s, } return omegas
def test_gyrofrequency(): r"""Test the gyrofrequency function in frequencies.py.""" assert gyrofrequency(B, "e-").unit.is_equivalent(u.rad / u.s) assert gyrofrequency(B, "e-", to_hz=True).unit.is_equivalent(u.Hz) assert np.isclose(gyrofrequency(1 * u.T, "e-").value, 175882008784.72018) assert np.isclose(gyrofrequency(2.4 * u.T, "e-").value, 422116821083.3284) assert np.isclose( gyrofrequency(1 * u.T, "e-", to_hz=True).value, 27992490076.528206) assert np.isclose( gyrofrequency(2.4 * u.T, "e-", signed=True).value, -422116821083.3284) assert np.isclose(gyrofrequency(1 * u.G, "e-").cgs.value, 1.76e7, rtol=1e-3) with pytest.raises(TypeError): with pytest.warns(u.UnitsWarning): gyrofrequency(u.m, "e-") with pytest.raises(u.UnitTypeError): gyrofrequency(u.m * 1, "e-") assert np.isnan(gyrofrequency(B_nanarr, "e-")[-1]) # The following is a test to check that equivalencies from astropy # are working. omega_ce = gyrofrequency(2.2 * u.T, "e-") f_ce = (omega_ce / (2 * np.pi)) / u.rad f_ce_use_equiv = omega_ce.to(u.Hz, equivalencies=[(u.cy / u.s, u.Hz)]) assert np.isclose(f_ce.value, f_ce_use_equiv.value) with pytest.warns(u.UnitsWarning): assert gyrofrequency(5.0, "e-") == gyrofrequency(5.0 * u.T, "e-") assert gyrofrequency(B, particle=ion).unit.is_equivalent(u.rad / u.s) assert np.isclose( gyrofrequency(1 * u.T, particle="p").value, 95788335.834874) assert np.isclose( gyrofrequency(2.4 * u.T, particle="p").value, 229892006.00369796) assert np.isclose(gyrofrequency(1 * u.G, particle="p").cgs.value, 9.58e3, rtol=2e-3) assert gyrofrequency(-5 * u.T, "p") == gyrofrequency(5 * u.T, "p") # Case when Z=1 is assumed # assert gyrofrequency(B, particle='p+') == gyrofrequency(B, particle='H-1') assert gyrofrequency(B, particle="e+") == gyrofrequency(B, "e-") with pytest.warns(u.UnitsWarning): gyrofrequency(8, "p") with pytest.raises(u.UnitTypeError): gyrofrequency(5 * u.m, "p") with pytest.raises(InvalidParticleError): gyrofrequency(8 * u.T, particle="asdfasd") with pytest.warns(u.UnitsWarning): # TODO this should be WARNS, not RAISES. and it's probably still raised assert gyrofrequency(5.0, "p") == gyrofrequency(5.0 * u.T, "p") gyrofrequency(1 * u.T, particle="p") # testing for user input Z testMeth1 = gyrofrequency(1 * u.T, particle="p", Z=0.8).si.value testTrue1 = 76630665.79318453 errStr = f"gyrofrequency() gave {testMeth1}, should be {testTrue1}." assert np.isclose(testMeth1, testTrue1, atol=0.0, rtol=1e-5), errStr assert_can_handle_nparray(gyrofrequency, kwargs={"signed": True}) assert_can_handle_nparray(gyrofrequency, kwargs={"signed": False})
def cold_plasma_permittivity_SDP(B: u.T, species, n, omega: u.rad / u.s): r""" Magnetized cold plasma dielectric permittivity tensor elements. Elements (S, D, P) are given in the "Stix" frame, i.e. with :math:`B ∥ \hat{z}` :cite:p:`stix:1992`. The :math:`\exp(-i ω t)` time-harmonic convention is assumed. Parameters ---------- B : `~astropy.units.Quantity` Magnetic field magnitude in units convertible to tesla. species : `list` of `str` List of the plasma particle species, e.g.: ``['e', 'D+']`` or ``['e', 'D+', 'He+']``. n : `list` of `~astropy.units.Quantity` `list` of species density in units convertible to per cubic meter The order of the species densities should follow species. omega : `~astropy.units.Quantity` Electromagnetic wave frequency in rad/s. Returns ------- sum : `~astropy.units.Quantity` S ("Sum") dielectric tensor element. difference : `~astropy.units.Quantity` D ("Difference") dielectric tensor element. plasma : `~astropy.units.Quantity` P ("Plasma") dielectric tensor element. Notes ----- The dielectric permittivity tensor is expressed in the Stix frame with the :math:`\exp(-i ω t)` time-harmonic convention as :math:`ε = ε_0 A`, with :math:`A` being .. math:: ε = ε_0 \left(\begin{matrix} S & -i D & 0 \\ +i D & S & 0 \\ 0 & 0 & P \end{matrix}\right) where: .. math:: S = 1 - \sum_s \frac{ω_{p,s}^2}{ω^2 - Ω_{c,s}^2} D = \sum_s \frac{Ω_{c,s}}{ω} \frac{ω_{p,s}^2}{ω^2 - Ω_{c,s}^2} P = 1 - \sum_s \frac{ω_{p,s}^2}{ω^2} where :math:`ω_{p,s}` is the plasma frequency and :math:`Ω_{c,s}` is the signed version of the cyclotron frequency for the species :math:`s`. Examples -------- >>> from astropy import units as u >>> from numpy import pi >>> B = 2*u.T >>> species = ['e', 'D+'] >>> n = [1e18*u.m**-3, 1e18*u.m**-3] >>> omega = 3.7e9*(2*pi)*(u.rad/u.s) >>> permittivity = S, D, P = cold_plasma_permittivity_SDP(B, species, n, omega) >>> S <Quantity 1.02422...> >>> permittivity.sum # namedtuple-style access <Quantity 1.02422...> >>> D <Quantity 0.39089...> >>> P <Quantity -4.8903...> """ S, D, P = 1, 0, 1 for s, n_s in zip(species, n): omega_c = gyrofrequency(B=B, particle=s, signed=True) omega_p = plasma_frequency(n=n_s, particle=s) S += -(omega_p**2) / (omega**2 - omega_c**2) D += omega_c / omega * omega_p**2 / (omega**2 - omega_c**2) P += -(omega_p**2) / omega**2 return StixTensorElements(S, D, P)
def cold_plasma_permittivity_LRP(B: u.T, species, n, omega: u.rad / u.s): r""" Magnetized cold plasma dielectric permittivity tensor elements. Elements (L, R, P) are given in the "rotating" basis, i.e. in the basis :math:`(\mathbf{u}_{+}, \mathbf{u}_{-}, \mathbf{u}_z)`, where the tensor is diagonal and with :math:`B ∥ z`\ . The :math:`\exp(-i ω t)` time-harmonic convention is assumed. Parameters ---------- B : `~astropy.units.Quantity` Magnetic field magnitude in units convertible to tesla. species : `list` of `str` The plasma particle species (e.g.: ``['e', 'D+']`` or ``['e', 'D+', 'He+']``. n : `list` of `~astropy.units.Quantity` `list` of species density in units convertible to per cubic meter. The order of the species densities should follow species. omega : `~astropy.units.Quantity` Electromagnetic wave frequency in rad/s. Returns ------- left : `~astropy.units.Quantity` L ("Left") Left-handed circularly polarization tensor element. right : `~astropy.units.Quantity` R ("Right") Right-handed circularly polarization tensor element. plasma : `~astropy.units.Quantity` P ("Plasma") dielectric tensor element. Notes ----- In the rotating frame defined by :math:`(\mathbf{u}_{+}, \mathbf{u}_{-}, \mathbf{u}_z)` with :math:`\mathbf{u}_{\pm}=(\mathbf{u}_x \pm \mathbf{u}_y)/\sqrt{2}`, the dielectric tensor takes a diagonal form with elements L, R, P with: .. math:: L = 1 - \sum_s \frac{ω_{p,s}^2}{ω\left(ω - Ω_{c,s}\right)} R = 1 - \sum_s \frac{ω_{p,s}^2}{ω\left(ω + Ω_{c,s}\right)} P = 1 - \sum_s \frac{ω_{p,s}^2}{ω^2} where :math:`ω_{p,s}` is the plasma frequency and :math:`Ω_{c,s}` is the signed version of the cyclotron frequency for the species :math:`s` :cite:p:`stix:1992`. Examples -------- >>> from astropy import units as u >>> from numpy import pi >>> B = 2*u.T >>> species = ['e', 'D+'] >>> n = [1e18*u.m**-3, 1e18*u.m**-3] >>> omega = 3.7e9*(2*pi)*(u.rad/u.s) >>> L, R, P = permittivity = cold_plasma_permittivity_LRP(B, species, n, omega) >>> L <Quantity 0.63333...> >>> permittivity.left # namedtuple-style access <Quantity 0.63333...> >>> R <Quantity 1.41512...> >>> P <Quantity -4.8903...> """ L, R, P = 1, 1, 1 for s, n_s in zip(species, n): omega_c = gyrofrequency(B=B, particle=s, signed=True) omega_p = plasma_frequency(n=n_s, particle=s) L += -(omega_p**2) / (omega * (omega - omega_c)) R += -(omega_p**2) / (omega * (omega + omega_c)) P += -(omega_p**2) / omega**2 return RotatingTensorElements(L, R, P)
def Hall_parameter( n: u.m**-3, T: u.K, B: u.T, ion: Particle, particle: Particle, coulomb_log=None, V=None, coulomb_log_method="classical", ): r""" Calculate the ``particle`` Hall parameter for a plasma. The Hall parameter for plasma species :math:`s` (``particle``) is given by: .. math:: β_{s} = \frac{Ω_{c s}}{ν_{s s^{\prime}}} where :math:`Ω_{c s}` is the gyrofrequncy for plasma species :math:`s` (``particle``) and :math:`ν_{s s^{\prime}}` is the collision frequency between plasma species :math:`s` (``particle``) and species :math:`s^{\prime}` (``ion``). **Aliases:** `betaH_` Parameters ---------- n : `~astropy.units.quantity.Quantity` The number density associated with ``particle``. T : `~astropy.units.quantity.Quantity` The temperature of associated with ``particle``. B : `~astropy.units.quantity.Quantity` The magnetic field. ion : `~plasmapy.particles.particle_class.Particle` The type of ion ``particle`` is colliding with. particle : `~plasmapy.particles.particle_class.Particle` The particle species for which the Hall parameter is calculated for. Representation of the particle species (e.g., ``'p'`` for protons, ``'D+'`` for deuterium, or ``'He-4 +1'`` for singly ionized helium-4). If no charge state information is provided, then the particles are assumed to be singly charged. coulomb_log : `float`, optional Preset value for the Coulomb logarithm. Used mostly for testing purposes. V : `~astropy.units.quantity.Quantity` The relative velocity between ``particle`` and ``ion``. If not provided, then the ``particle`` thermal velocity is assumed (`~plasmapy.formulary.speeds.thermal_speed`). coulomb_log_method : `str`, optional The method by which to compute the Coulomb logarithm. The default method is the classical straight-line Landau-Spitzer method (``"classical"`` or ``"ls"``). The other 6 supported methods are ``"ls_min_interp"``, ``"ls_full_interp"``, ``"ls_clamp_mininterp"``, ``"hls_min_interp"``, ``"hls_max_interp"``, and ``"hls_full_interp"``. Please refer to the docstring of `~plasmapy.formulary.collisions.Coulomb_logarithm` for more information about these methods. See Also -------- ~plasmapy.formulary.frequencies.gyrofrequency ~plasmapy.formulary.collisions.fundamental_electron_collision_freq ~plasmapy.formulary.collisions.fundamental_ion_collision_freq ~plasmapy.formulary.collisions.Coulomb_logarithm Returns ------- `~astropy.units.quantity.Quantity` Hall parameter for ``particle``. Notes ----- * For calculating the collision frequency `~plasmapy.formulary.collisions.fundamental_electron_collision_freq` is used when ``particle`` is an electron and `~plasmapy.formulary.collisions.fundamental_ion_collision_freq` when ``particle`` is an ion. * The collision frequencies are calculated assuming a slowly moving Maxwellian distribution. Examples -------- >>> import astropy.units as u >>> import pytest >>> from plasmapy.utils.exceptions import RelativityWarning >>> Hall_parameter(1e10 * u.m**-3, 2.8e2 * u.eV, 2.3 * u.T, 'He-4 +1', 'e-') <Quantity 2.500...e+15> >>> with pytest.warns(RelativityWarning): ... Hall_parameter(1e10 * u.m**-3, 5.8e3 * u.eV, 2.3 * u.T, 'He-4 +1', 'e-') <Quantity 2.11158...e+17> """ from plasmapy.formulary.collisions import ( fundamental_electron_collision_freq, fundamental_ion_collision_freq, ) gyro_frequency = frequencies.gyrofrequency(B, particle) gyro_frequency = gyro_frequency / u.radian if Particle(particle).symbol == "e-": coll_rate = fundamental_electron_collision_freq( T, n, ion, coulomb_log, V, coulomb_log_method=coulomb_log_method) else: coll_rate = fundamental_ion_collision_freq(T, n, ion, coulomb_log, V) return gyro_frequency / coll_rate
if theta.ndim not in (0, 1): raise TypeError( "Argument 'theta' needs to be a single value or 1D array " f" astropy Quantity, got array of shape {theta.shape}.") elif np.isscalar(theta): theta = np.array([theta]) # Generate mesh grid of w x theta w, theta = np.meshgrid(w, theta, indexing="ij") # 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) R = S + D L = S - D # Generate coefficients to solve, a * k**4 + b * k**2 + c = 0 a = (S * np.sin(theta)**2) + (P * np.cos(theta)**2)