def test_roundtrip( cubefrequencies=[218.44005, 234.68345, 220.07849, 234.69847, 231.28115] * u.GHz, degeneracies=[9, 9, 17, 11, 21], xaxis=[45.45959683, 60.92357159, 96.61387286, 122.72191958, 165.34856457] * u.K, indices=[3503, 1504, 2500, 116, 3322], ): # integrated line over 1 km/s (see dnu) onekms = 1 * u.km / u.s / constants.c kkms = ( lte_molecule.line_brightness( tex=100 * u.K, total_column=1e15 * u.cm ** -2, partition_function=1185, degeneracy=degeneracies, frequency=cubefrequencies, energy_upper=xaxis.to(u.erg, u.temperature_energy()), einstein_A=einsteinAij[indices], dnu=onekms * cubefrequencies, ) * u.km / u.s ) col, tem, slope, intcpt = fit_tex( xaxis, nupper_of_kkms(kkms, cubefrequencies, einsteinAij[indices], degeneracies).value, plot=True ) print("temperature = {0} (input was 100)".format(tem)) print("column = {0} (input was 1e15)".format(np.log10(col.value)))
def test_add_equivelencies(): e1 = u.pixel_scale(10*u.arcsec/u.pixel) + u.temperature_energy() assert isinstance(e1, Equivalency) assert e1.name == ["pixel_scale", "temperature_energy"] assert isinstance(e1.kwargs, list) assert e1.kwargs == [dict({'pixscale': 10*u.arcsec/u.pix}), dict()] e2 = u.pixel_scale(10*u.arcsec/u.pixel) + [1, 2,3] assert isinstance(e2, list)
def thermal_deBroglie_wavelength(T_e): r""" Calculate the thermal deBroglie wavelength for electrons. Parameters ---------- T_e: ~astropy.units.Quantity Electron temperature. Returns ------- lambda_dbTh: ~astropy.units.Quantity The thermal deBroglie wavelength for electrons in meters. Raises ------ TypeError If argument is not a `~astropy.units.Quantity`. ~astropy.units.UnitConversionError If argument is in incorrect units. ValueError If argument contains invalid values. Warns ----- UserWarning If units are not provided and SI units are assumed. Notes ----- The thermal deBroglie wavelength is approximately the average deBroglie wavelength for electrons in an ideal gas and is given by .. math:: \lambda_dbTh = \frac{h}{\sqrt{2 \pi m_e k_B T_e}} Example ------- >>> from astropy import units as u >>> thermal_deBroglie_wavelength(1 * u.eV) <Quantity 6.91936752e-10 m> """ T_e = T_e.to(u.K, equivalencies=u.temperature_energy()) lambda_dbTh = h / np.sqrt(2 * np.pi * m_e * k_B * T_e) return lambda_dbTh.to(u.m)
def model(tex, col, einsteinAs=einsteinAs, eupper=eupper): tex = u.Quantity(tex, u.K) col = u.Quantity(col, u.cm**-2) eupper = eupper.to(u.erg, u.temperature_energy()) einsteinAs = u.Quantity(einsteinAs, u.Hz) partition_func = specmodel.calculate_partitionfunction(hnco.data['States'], temperature=tex.value) assert len(partition_func) == 1 Q_rot = tuple(partition_func.values())[0] return lte_molecule.line_brightness(tex, bandwidth, frequencies, total_column=col, partition_function=Q_rot, degeneracy=degeneracies, energy_upper=eupper, einstein_A=einsteinAs)
def test_unitless_no_vTh(self): """ Tests distribution function without units, and not passing vTh. """ # converting T to SI then stripping units T = self.T.to(u.K, equivalencies=u.temperature_energy()) T = T.si.value distFunc = Maxwellian_speed_1D(v=self.v.si.value, T=T, particle=self.particle, units="unitless") errStr = (f"Distribution function should be {self.distFuncTrue} " f"and not {distFunc}.") assert np.isclose(distFunc, self.distFuncTrue, rtol=1e-8, atol=0.0), errStr
def test_unitless_vTh(self): """ Tests distribution function without units, and with passing vTh. """ # converting T to SI then stripping units T_e = self.T_e.to(u.K, equivalencies=u.temperature_energy()) T_e = T_e.si.value distFunc = kappa_velocity_1D(v=self.v.si.value, T=T_e, kappa=self.kappa, vTh=self.vTh.si.value, particle=self.particle, units="unitless") errStr = (f"Distribution function should be {self.distFuncTrue} " f"and not {distFunc}.") assert np.isclose(distFunc, self.distFuncTrue, rtol=1e-8, atol=0.0), errStr
def getLEnergy(self,index=None,unit=1./u.cm): ''' Return the energies of the <ny_up+ny_low> excitation levels. In case a single energy level is requested via index the energy value is extracted from the array. @keyword index: The index. In case of default, all are returned. Can be any array-like object that includes indices (default: None) @type index: int/array @keyword unit: The unit of the returned values. Can be any valid units str from the astropy units module (energy), or the unit itself. 'cm-1' and 'cm^-1' are accepted as well. (default: 1./u.cm) @type unit: string/unit @return: The energies ordered by level index. @rtype: float/array ''' #-- Get the energies in cm^-1 energies = self.get('level','energy',index) #-- Convert the units. Grab the unit first if isinstance(unit,str) and unit.lower() in ['cm-1','cm^-1']: unit = 1./u.cm elif isinstance(unit,str): unit = getattr(u,unit) #-- In case of temperature, and extra step is needed if (isinstance(unit,u.Quantity) and unit.unit.is_equivalent(u.K)) \ or (isinstance(unit,u.UnitBase) and unit.is_equivalent(u.K)): energies = (energies/u.cm).to(u.erg,equivalencies=u.spectral()) return energies.to(unit,equivalencies=u.temperature_energy()).value else: return (energies/u.cm).to(unit,equivalencies=u.spectral()).value
def getWavelength(self, species, unit="micron"): """ Return the wavelength grid for a given species. @param species: The dust species (from Dust.dat) @type species: string @keyword unit: The unit of the wavelength. Can be given as u.Unit() object or as a string representation of those objects. Can range from length, to frequency, and energy (default: micron) @type unit: str/u.Unit() @return: wavelength (given unit) @rtype: array """ # -- Convert the units. Grab the unit first if isinstance(unit, str) and unit.lower() in ["cm-1", "cm^-1"]: unit = 1.0 / u.cm elif isinstance(unit, str): unit = getattr(u, unit) self.readKappas(species) if not self.waves.has_key(species): return np.empty(0) wav = self.waves[species] * u.micron # -- In case of temperature, and extra step is needed if (isinstance(unit, u.Quantity) and unit.unit.is_equivalent(u.K)) or ( isinstance(unit, u.UnitBase) and unit.is_equivalent(u.K) ): wav = wav.to(u.erg, equivalencies=u.spectral()) return wav.to(unit, equivalencies=u.temperature_energy()).value else: return wav.to(unit, equivalencies=u.spectral()).value
def test_Debye_number(): r"""Test the Debye_number function in parameters.py.""" assert Debye_number(T_e, n_e).unit == u.dimensionless_unscaled T_e_eV = T_e.to(u.eV, equivalencies=u.temperature_energy()) assert np.isclose(Debye_number(T_e, n_e).value, Debye_number(T_e_eV, n_e).value) assert np.isclose(Debye_number( 1 * u.eV, 1 * u.cm**-3).value, 1720862385.43342) with pytest.raises(UserWarning): Debye_number(T_e, 4) with pytest.raises(TypeError): Debye_number(None, n_e) with pytest.raises(u.UnitConversionError): Debye_number(5 * u.m, 5 * u.m**-3) with pytest.raises(u.UnitConversionError): Debye_number(5 * u.K, 5 * u.m**3) with pytest.raises(ValueError): Debye_number(5j * u.K, 5 * u.cm**-3) Tarr2 = np.array([1, 2]) * u.K narr3 = np.array([1, 2, 3]) * u.m**-3 with pytest.raises(ValueError): Debye_number(Tarr2, narr3) with pytest.raises(UserWarning): assert Debye_number(1.1, 1.1) == Debye_number(1.1 * u.K, 1.1 * u.m**-3) with pytest.raises(UserWarning): assert Debye_number(1.1 * u.K, 1.1) == Debye_number(1.1, 1.1 * u.m**-3)
def test_Debye_number(): r"""Test the Debye_number function in parameters.py.""" assert Debye_number(T_e, n_e).unit.is_equivalent(u.dimensionless_unscaled) T_e_eV = T_e.to(u.eV, equivalencies=u.temperature_energy()) assert np.isclose(Debye_number(T_e, n_e).value, Debye_number(T_e_eV, n_e).value) assert np.isclose(Debye_number(1 * u.eV, 1 * u.cm ** -3).value, 1720862385.43342) with pytest.warns(u.UnitsWarning): Debye_number(T_e, 4) with pytest.raises(ValueError): Debye_number(None, n_e) with pytest.raises(u.UnitTypeError): Debye_number(5 * u.m, 5 * u.m ** -3) with pytest.raises(u.UnitTypeError): Debye_number(5 * u.K, 5 * u.m ** 3) with pytest.raises(ValueError): Debye_number(5j * u.K, 5 * u.cm ** -3) Tarr2 = np.array([1, 2]) * u.K narr3 = np.array([1, 2, 3]) * u.m ** -3 with pytest.raises(ValueError): Debye_number(Tarr2, narr3) with pytest.warns(u.UnitsWarning): assert Debye_number(1.1, 1.1) == Debye_number(1.1 * u.K, 1.1 * u.m ** -3) with pytest.warns(u.UnitsWarning): assert Debye_number(1.1 * u.K, 1.1) == Debye_number(1.1, 1.1 * u.m ** -3) assert_can_handle_nparray(Debye_number)
def test_roundtrip(cubefrequencies=[218.44005, 234.68345, 220.07849, 234.69847, 231.28115]*u.GHz, degeneracies=[9, 9, 17, 11, 21], xaxis=[45.45959683, 60.92357159, 96.61387286, 122.72191958, 165.34856457]*u.K, indices=[3503, 1504, 2500, 116, 3322], ): # integrated line over 1 km/s (see dnu) onekms = 1*u.km/u.s / constants.c kkms = lte_molecule.line_brightness(tex=100*u.K, total_column=1e15*u.cm**-2, partition_function=1185, degeneracy=degeneracies, frequency=cubefrequencies, energy_upper=xaxis.to(u.erg, u.temperature_energy()), einstein_A=einsteinAij[indices], dnu=onekms*cubefrequencies) * u.km/u.s col, tem, slope, intcpt = fit_tex(xaxis, nupper_of_kkms(kkms, cubefrequencies, einsteinAij[indices], degeneracies).value, plot=True) print("temperature = {0} (input was 100)".format(tem)) print("column = {0} (input was 1e15)".format(np.log10(col.value)))
def gyroradius(B, *args, Vperp=None, T_i=None, particle='e'): r"""Returns the particle gyroradius. Parameters ---------- B: Quantity The magnetic field magnitude in units convertible to tesla. Vperp: Quantity, optional The component of particle velocity that is perpendicular to the magnetic field in units convertible to meters per second. T_i: Quantity, optional The particle temperature in units convertible to kelvin. particle : string, optional Representation of the particle species (e.g., 'p' for protons, 'D+' for deuterium, or 'He-4 +1' for singly ionized helium-4), which defaults to electrons. If no charge state information is provided, then the particles are assumed to be singly charged. args : Quantity If the second positional argument is a Quantity with units appropriate to Vperp or T_i, then this argument will take the place of that keyword argument. Returns ------- r_Li : Quantity The particle gyroradius in units of meters. This Quantity will be based on either the perpendicular component of particle velocity as inputted, or the most probable speed for an particle within a Maxwellian distribution for the particle temperature. Raises ------ TypeError The arguments are of an incorrect type UnitConversionError The arguments do not have appropriate units ValueError If any argument contains invalid values UserWarning If units are not provided and SI units are assumed Notes ----- One but not both of Vperp and T_i must be inputted. If any of B, Vperp, or T_i is a number rather than a Quantity, then SI units will be assumed and a warning will be raised. Formula ------- The particle gyroradius is also known as the particle Larmor radius and is given by .. math:: r_{Li} = \frac{V_{\perp}}{omega_{ci}} where :math:`V_{\perp}` is the component of particle velocity that is perpendicular to the magnetic field and :math:`\omega_{ci}` is the particle gyrofrequency. If a temperature is provided, then :math:`V_\perp` will be the most probable thermal velocity of an particle at that temperature. Examples -------- >>> from astropy import units as u >>> gyroradius(0.2*u.T, 1e5*u.K, particle='p') <Quantity 0.002120874971411475 m> >>> gyroradius(0.2*u.T, 1e5*u.K, particle='p') <Quantity 0.002120874971411475 m> >>> gyroradius(5*u.uG, 1*u.eV, particle='alpha') <Quantity 288002.38837768475 m> >>> gyroradius(400*u.G, 1e7*u.m/u.s, particle='Fe+++') <Quantity 48.23129811339086 m> >>> gyroradius(B = 0.01*u.T, T_i = 1e6*u.K) <Quantity 0.0031303339253265536 m> >>> gyroradius(B = 0.01*u.T, Vperp = 1e6*u.m/u.s) <Quantity 0.0005685630062091092 m> >>> gyroradius(0.2*u.T, 1e5*u.K) <Quantity 4.9494925204636764e-05 m> >>> gyroradius(5*u.uG, 1*u.eV) <Quantity 6744.259818299466 m> >>> gyroradius(400*u.G, 1e7*u.m/u.s) <Quantity 0.0014214075155227729 m> """ if Vperp is not None and T_i is not None: raise ValueError("Cannot have both Vperp and T_i as arguments to " "gyroradius") if len(args) == 1 and isinstance(args[0], units.Quantity): arg = args[0].si if arg.unit == units.T and B.si.unit in [ units.J, units.K, units.m / units.s ]: B, arg = arg, B if arg.unit == units.m / units.s: Vperp = arg elif arg.unit in (units.J, units.K): T_i = arg.to(units.K, equivalencies=units.temperature_energy()) else: raise units.UnitConversionError("Incorrect units for positional " "argument in gyroradius") elif len(args) > 0: raise ValueError("Incorrect inputs to gyroradius") _check_quantity(B, 'B', 'gyroradius', units.T) if Vperp is not None: _check_quantity(Vperp, 'Vperp', 'gyroradius', units.m / units.s) elif T_i is not None: _check_quantity(T_i, 'T_i', 'gyroradius', units.K) Vperp = thermal_speed(T_i, particle=particle) omega_ci = gyrofrequency(B, particle) r_Li = np.abs(Vperp) / omega_ci return r_Li.to(units.m, equivalencies=units.dimensionless_angles())
def ion_sound_speed(*ignore, T_e=0 * units.K, T_i=0 * units.K, gamma_e=1, gamma_i=3, ion='p'): r"""Returns the ion sound speed for an electron-ion plasma. Parameters ---------- T_e : Quantity, optional Electron temperature in units of temperature or energy per particle. If this is not given, then the electron temperature is assumed to be zero. If only one temperature is entered, it is assumed to be the electron temperature. T_i : Quantity, optional Ion temperature in units of temperature or energy per particle. If this is not given, then the ion temperature is assumed to be zero. gamma_e : float or int The adiabatic index for electrons, which defaults to 1. This value assumes that the electrons are able to equalize their temperature rapidly enough that the electrons are effectively isothermal. gamma_i : float or int The adiabatic index for ions, which defaults to 3. This value assumes that ion motion has only one degree of freedom, namely along magnetic field lines. ion : string, optional Representation of the ion species (e.g., 'p' for protons, 'D+' for deuterium, or 'He-4 +1' for singly ionized helium-4), which defaults to protons. If no charge state information is provided, then the ions are assumed to be singly charged. Returns ------- V_S : Quantity The ion sound speed in units of meters per second. Raises ------ TypeError If any of the arguments are not entered as keyword arguments or are of an incorrect type. ValueError If the ion mass, adiabatic index, or temperature are invalid. UnitConversionError If the temperature is in incorrect units. UserWarning If the ion sound speed exceeds 10% of the speed of light, or if units are not provided and SI units are assumed. Notes ----- The ion sound speed :math:`V_S` is approximately given by .. math:: V_S = \sqrt{\frac{\gamma_e Z k_B T_e + \gamma_i k_B T_i}{m_i}} where :math:`\gamma_e` and :math:`\gamma_i` are the electron and ion adiabatic indices, :math:`k_B` is the Boltzmann constant, :math:`T_e` and :math:`T_i` are the electron and ion temperatures, :math:`Z` is the charge state of the ion, and :math:`m_i` is the ion mass. This function assumes that the product of the wavenumber and the Debye length is small. In this limit, the ion sound speed is not dispersive (e.g., frequency independent). When the electron temperature is much greater than the ion temperature, the ion sound velocity reduces to :math:`\sqrt{\gamma_e k_B T_e / m_i}`. Ion acoustic waves can therefore occur even when the ion temperature is zero. Example ------- >>> from astropy import units as u >>> ion_sound_speed(T_e=5e6*u.K, T_i=0*u.K, ion='p', gamma_e=1, gamma_i=3) <Quantity 203155.07640420322 m / s> >>> ion_sound_speed(T_e=5e6*u.K) <Quantity 203155.07640420322 m / s> >>> ion_sound_speed(T_e=500*u.eV, T_i=200*u.eV, ion='D+') <Quantity 229586.01860212447 m / s> """ if ignore: raise TypeError("All arguments are required to be keyword arguments " "in ion_sound_speed to prevent mixing up the electron " "and ion temperatures. An example call that uses the " "units subpackage from astropy is: " "ion_sound_speed(T_e=5*units.K, T_i=0*units.K, " "ion='D+')") try: m_i = ion_mass(ion) Z = charge_state(ion) if Z is None: Z = 1 except Exception: raise ValueError("Invalid ion in ion_sound_speed.") if not isinstance(gamma_e, (float, int)): raise TypeError("The adiabatic index for electrons (gamma_e) must be " "a float or int in ion_sound_speed") if not isinstance(gamma_i, (float, int)): raise TypeError("The adiabatic index for ions (gamma_i) must be " "a float or int in ion_sound_speed") if not 1 <= gamma_e <= np.inf: raise ValueError("The adiabatic index for electrons must be between " "one and infinity") if not 1 <= gamma_i <= np.inf: raise ValueError("The adiabatic index for ions must be between " "one and infinity") T_i = T_i.to(units.K, equivalencies=units.temperature_energy()) T_e = T_e.to(units.K, equivalencies=units.temperature_energy()) try: V_S_squared = (gamma_e * Z * k_B * T_e + gamma_i * k_B * T_i) / m_i V_S = np.sqrt(V_S_squared).to(units.m / units.s) except Exception: raise ValueError("Unable to find ion sound speed.") return V_S
class IonizationStateCollection: """ Describe the ionization state distributions of multiple elements or isotopes. Parameters ---------- inputs: `list`, `tuple`, or `dict` A `list` or `tuple` of elements or isotopes (if ``T_e`` is provided); a `list` of `~plasmapy.particles.IonizationState` instances; a `dict` with elements or isotopes as keys and a `~numpy.ndarray` of ionic fractions as the values; or a `dict` with elements or isotopes as keys and `~astropy.units.Quantity` instances with units of number density. abundances: `dict`, optional, keyword-only A `dict` with `~plasmapy.particles.particle_class.ParticleLike` objects used as the keys and the corresponding relative abundance as the values. The values must be positive real numbers. log_abundances: `dict`, optional, keyword-only A `dict` with `~plasmapy.particles.particle_class.ParticleLike` objects used as the keys and the corresponding base 10 logarithms of their relative abundances as the values. The values must be real numbers. n0: `~astropy.units.Quantity`, optional, keyword-only The number density normalization factor corresponding to the abundances. The number density of each element is the product of its abundance and ``n0``. T_e: `~astropy.units.Quantity`, optional, keyword-only The electron temperature in units of temperature or thermal energy per particle. kappa: `float`, optional, keyword-only The value of kappa for a kappa distribution function. tol: `float` or `integer`, optional, keyword-only The absolute tolerance used by `~numpy.isclose` when testing normalizations and making comparisons. Defaults to ``1e-15``. Raises ------ `~plasmapy.particles.exceptions.ParticleError` If `~plasmapy.particles.IonizationStateCollection` cannot be instantiated. See Also -------- ~plasmapy.particles.ionization_state.IonicFraction ~plasmapy.particles.ionization_state.IonizationState Examples -------- >>> from astropy import units as u >>> from plasmapy.particles import IonizationStateCollection >>> states = IonizationStateCollection( ... {'H': [0.5, 0.5], 'He': [0.95, 0.05, 0]}, ... T_e = 1.2e4 * u.K, ... n0 = 1e15 * u.m ** -3, ... abundances = {'H': 1, 'He': 0.08}, ... ) >>> states.ionic_fractions {'H': array([0.5, 0.5]), 'He': array([0.95, 0.05, 0. ])} The number densities are given by the ionic fractions multiplied by the abundance and the number density scaling factor ``n0``. >>> states.number_densities['H'] <Quantity [5.e+14, 5.e+14] 1 / m3> >>> states['He'] = [0.4, 0.59, 0.01] To change the ionic fractions for a single element, use item assignment. >>> states = IonizationStateCollection(['H', 'He']) >>> states['H'] = [0.1, 0.9] Item assignment will also work if you supply number densities. >>> states['He'] = [0.4, 0.6, 0.0] * u.m ** -3 >>> states.ionic_fractions['He'] array([0.4, 0.6, 0. ]) >>> states.number_densities['He'] <Quantity [0.4, 0.6, 0. ] 1 / m3> Notes ----- No more than one of ``abundances`` and ``log_abundances`` may be specified. If the value provided during item assignment is a `~astropy.units.Quantity` with units of number density that retains the total element density, then the ionic fractions will be set proportionately. When making comparisons between `~plasmapy.particles.IonizationStateCollection` instances, `~numpy.nan` values are treated as equal. Equality tests are performed to within a tolerance of ``tol``. """ # TODO: Improve explanation of dunder methods in docstring # TODO: Add functionality to equilibrate initial ionization states @validate_quantities(T_e={"equivalencies": u.temperature_energy()}) def __init__( self, inputs: Union[Dict[str, np.ndarray], List, Tuple], *, T_e: u.K = np.nan * u.K, abundances: Optional[Dict[str, Real]] = None, log_abundances: Optional[Dict[str, Real]] = None, n0: u.m ** -3 = np.nan * u.m ** -3, tol: Real = 1e-15, kappa: Real = np.inf, ): abundances_provided = abundances is not None or log_abundances is not None set_abundances = True if isinstance(inputs, dict): all_quantities = np.all( [isinstance(fracs, u.Quantity) for fracs in inputs.values()] ) if all_quantities: right_units = np.all( [fracs[0].si.unit == u.m ** -3 for fracs in inputs.values()] ) if not right_units: raise ParticleError( "Units must be inverse volume for number densities." ) if abundances_provided: raise ParticleError( "Abundances cannot be provided if inputs " "provides number density information." ) set_abundances = False try: self._pars = dict() self.T_e = T_e self.n0 = n0 self.tol = tol self.ionic_fractions = inputs if set_abundances: self.abundances = abundances self.log_abundances = log_abundances self.kappa = kappa except Exception as exc: raise ParticleError( "Unable to create IonizationStateCollection object." ) from exc def __str__(self) -> str: return f"<IonizationStateCollection for: {', '.join(self.base_particles)}>" def __repr__(self) -> str: return self.__str__() def __getitem__(self, *values) -> Union[IonizationState, IonicFraction]: errmsg = f"Invalid indexing for IonizationStateCollection instance: {values[0]}" one_input = not isinstance(values[0], tuple) two_inputs = len(values[0]) == 2 if not one_input and not two_inputs: raise IndexError(errmsg) try: arg1 = values[0] if one_input else values[0][0] int_charge = None if one_input else values[0][1] particle = arg1 if arg1 in self.base_particles else particle_symbol(arg1) if int_charge is None: return IonizationState( particle=particle, ionic_fractions=self.ionic_fractions[particle], T_e=self._pars["T_e"], n_elem=np.sum(self.number_densities[particle]), tol=self.tol, ) else: if not isinstance(int_charge, Integral): raise TypeError( f"{int_charge} is not a valid charge for {particle}." ) elif not 0 <= int_charge <= atomic_number(particle): raise ChargeError( f"{int_charge} is not a valid charge for {particle}." ) return IonicFraction( ion=particle_symbol(particle, Z=int_charge), ionic_fraction=self.ionic_fractions[particle][int_charge], number_density=self.number_densities[particle][int_charge], ) except Exception as exc: raise IndexError(errmsg) from exc def __setitem__(self, key, value): errmsg = ( f"Cannot set item for this IonizationStateCollection instance for " f"key = {repr(key)} and value = {repr(value)}" ) try: particle = particle_symbol(key) self.ionic_fractions[key] except (ParticleError, TypeError): raise KeyError( f"{errmsg} because {repr(key)} is an invalid particle." ) from None except KeyError: raise KeyError( f"{errmsg} because {repr(key)} is not one of the base " f"particles whose ionization state is being kept track " f"of." ) from None if isinstance(value, u.Quantity) and value.unit != u.dimensionless_unscaled: try: new_number_densities = value.to(u.m ** -3) except u.UnitConversionError: raise ValueError( f"{errmsg} because the units of value do not " f"correspond to a number density." ) from None old_n_elem = np.sum(self.number_densities[particle]) new_n_elem = np.sum(new_number_densities) density_was_nan = np.all(np.isnan(self.number_densities[particle])) same_density = u.quantity.allclose(old_n_elem, new_n_elem, rtol=self.tol) if not same_density and not density_was_nan: raise ValueError( f"{errmsg} because the old element number density " f"of {old_n_elem} is not approximately equal to " f"the new element number density of {new_n_elem}." ) value = (new_number_densities / new_n_elem).to(u.dimensionless_unscaled) # If the abundance of this particle has not been defined, # then set the abundance if there is enough (but not too # much) information to do so. abundance_is_undefined = np.isnan(self.abundances[particle]) isnan_of_abundance_values = np.isnan(list(self.abundances.values())) all_abundances_are_nan = np.all(isnan_of_abundance_values) n_is_defined = not np.isnan(self.n0) if abundance_is_undefined: if n_is_defined: self._pars["abundances"][particle] = new_n_elem / self.n0 elif all_abundances_are_nan: self.n0 = new_n_elem self._pars["abundances"][particle] = 1 else: raise ParticleError( f"Cannot set number density of {particle} to " f"{value * new_n_elem} when the number density " f"scaling factor is undefined, the abundance " f"of {particle} is undefined, and some of the " f"abundances of other elements/isotopes is " f"defined." ) try: new_fractions = np.array(value, dtype=np.float64) except Exception as exc: raise TypeError( f"{errmsg} because value cannot be converted into an " f"array that represents ionic fractions." ) from exc # TODO: Create a separate function that makes sure ionic # TODO: fractions are valid to reduce code repetition. This # TODO: would probably best go as a private function in # TODO: ionization_state.py. required_nstates = atomic_number(particle) + 1 new_nstates = len(new_fractions) if new_nstates != required_nstates: raise ValueError( f"{errmsg} because value must have {required_nstates} " f"ionization levels but instead corresponds to " f"{new_nstates} levels." ) all_nans = np.all(np.isnan(new_fractions)) if not all_nans and (new_fractions.min() < 0 or new_fractions.max() > 1): raise ValueError( f"{errmsg} because the new ionic fractions are not " f"all between 0 and 1." ) normalized = np.isclose(np.sum(new_fractions), 1, rtol=self.tol) if not normalized and not all_nans: raise ValueError( f"{errmsg} because the ionic fractions are not normalized to one." ) self._ionic_fractions[particle][:] = new_fractions[:] def __iter__(self): yield from [self[key] for key in self.ionic_fractions.keys()] def __eq__(self, other): if not isinstance(other, IonizationStateCollection): raise TypeError( "IonizationStateCollection instance can only be compared with " "other IonizationStateCollection instances." ) if self.base_particles != other.base_particles: raise ParticleError( "Two IonizationStateCollection instances can be compared only " "if the base particles are the same." ) min_tol = np.min([self.tol, other.tol]) # Check any of a whole bunch of equality measures, recalling # that np.nan == np.nan is False. for attribute in ["T_e", "n_e", "kappa"]: this = eval(f"self.{attribute}") that = eval(f"other.{attribute}") # TODO: Maybe create a function in utils called same_enough # TODO: that would take care of all of these disparate # TODO: equality measures. this_equals_that = np.any( [ this == that, this is that, np.isnan(this) and np.isnan(that), np.isinf(this) and np.isinf(that), u.quantity.allclose(this, that, rtol=min_tol), ] ) if not this_equals_that: return False for attribute in ["ionic_fractions", "number_densities"]: this_dict = eval(f"self.{attribute}") that_dict = eval(f"other.{attribute}") for particle in self.base_particles: this = this_dict[particle] that = that_dict[particle] this_equals_that = np.any( [ this is that, np.all(np.isnan(this)) and np.all(np.isnan(that)), u.quantity.allclose(this, that, rtol=min_tol), ] ) if not this_equals_that: return False return True @property def ionic_fractions(self) -> Dict[str, np.array]: """ Return a `dict` containing the ionic fractions for each element and isotope. The keys of this `dict` are the symbols for each element or isotope. The values will be `~numpy.ndarray` objects containing the ionic fractions for each ionization level corresponding to each element or isotope. """ return self._ionic_fractions @ionic_fractions.setter def ionic_fractions(self, inputs: Union[Dict, List, Tuple]): """ Set the ionic fractions. Notes ----- The ionic fractions are initialized during instantiation of `~plasmapy.particles.IonizationStateCollection`. After this, the only way to reset the ionic fractions via the ``ionic_fractions`` attribute is via a `dict` with elements or isotopes that are a superset of the previous elements or isotopes. However, you may use item assignment of the `~plasmapy.particles.IonizationState` instance to assign new ionic fractions one element or isotope at a time. Raises ------ `~plasmapy.particles.exceptions.ParticleError` If the ionic fractions cannot be set. `TypeError` If ``inputs`` is not a `list`, `tuple`, or `dict` during instantiation, or if ``inputs`` is not a `dict` when it is being set. """ # A potential problem is that using item assignment on the # ionic_fractions attribute could cause the original attributes # to be overwritten without checks being performed. We might # eventually want to create a new class or subclass of UserDict # that goes through these checks. In the meantime, we should # make it clear to users to set ionic_fractions by using item # assignment on the IonizationStateCollection instance as a whole. An # example of the problem is `s = IonizationStateCollection(["He"])` being # followed by `s.ionic_fractions["He"] = 0.3`. if hasattr(self, "_ionic_fractions"): if not isinstance(inputs, dict): raise TypeError( "Can only reset ionic_fractions with a dict if " "ionic_fractions has been set already." ) old_particles = set(self.base_particles) new_particles = {particle_symbol(key) for key in inputs.keys()} missing_particles = old_particles - new_particles if missing_particles: raise ParticleError( "Can only reset ionic fractions with a dict if " "the new base particles are a superset of the " "prior base particles. To change ionic fractions " "for one base particle, use item assignment on the " "IonizationStateCollection instance instead." ) if isinstance(inputs, dict): original_keys = inputs.keys() ionfrac_types = {type(inputs[key]) for key in original_keys} inputs_have_quantities = u.Quantity in ionfrac_types if inputs_have_quantities and len(ionfrac_types) != 1: raise TypeError( "Ionic fraction information may only be inputted " "as a Quantity object if all ionic fractions are " "Quantity arrays with units of inverse volume." ) try: particles = {key: Particle(key) for key in original_keys} except (InvalidParticleError, TypeError) as exc: raise ParticleError( "Unable to create IonizationStateCollection instance " "because not all particles are valid." ) from exc # The particles whose ionization states are to be recorded # should be elements or isotopes but not ions or neutrals. for key in particles.keys(): is_element = particles[key].is_category("element") has_charge_info = particles[key].is_category( any_of=["charged", "uncharged"] ) if not is_element or has_charge_info: raise ParticleError( f"{key} is not an element or isotope without " f"charge information." ) # We are sorting the elements/isotopes by atomic number and # mass number since we will often want to plot and analyze # things and this is the most sensible order. def _sort_entries_by_atomic_and_mass_numbers(k): return ( particles[k].atomic_number, particles[k].mass_number if particles[k].isotope else 0, ) sorted_keys = sorted( original_keys, key=_sort_entries_by_atomic_and_mass_numbers ) _elements_and_isotopes = [] _particle_instances = [] new_ionic_fractions = {} if inputs_have_quantities: n_elems = {} for key in sorted_keys: new_key = particles[key].symbol _particle_instances.append(particles[key]) if new_key in _elements_and_isotopes: raise ParticleError( "Repeated particles in IonizationStateCollection." ) nstates_input = len(inputs[key]) nstates = particles[key].atomic_number + 1 if nstates != nstates_input: raise ParticleError( f"The ionic fractions array for {key} must " f"have a length of {nstates}." ) _elements_and_isotopes.append(new_key) if inputs_have_quantities: try: number_densities = inputs[key].to(u.m ** -3) n_elem = np.sum(number_densities) new_ionic_fractions[new_key] = np.array( number_densities / n_elem ) n_elems[key] = n_elem except u.UnitConversionError as exc: raise ParticleError("Units are not inverse volume.") from exc elif ( isinstance(inputs[key], np.ndarray) and inputs[key].dtype.kind == "f" ): new_ionic_fractions[particles[key].symbol] = inputs[key] else: try: new_ionic_fractions[particles[key].symbol] = np.array( inputs[key], dtype=np.float ) except ValueError as exc: raise ParticleError( f"Inappropriate ionic fractions for {key}." ) from exc for key in _elements_and_isotopes: fractions = new_ionic_fractions[key] if not np.all(np.isnan(fractions)): if np.min(fractions) < 0 or np.max(fractions) > 1: raise ParticleError( f"Ionic fractions for {key} are not between 0 and 1." ) if not np.isclose(np.sum(fractions), 1, atol=self.tol, rtol=0): raise ParticleError( f"Ionic fractions for {key} are not normalized to 1." ) # When the inputs provide the densities, the abundances must # not have been provided because that would be redundant # or contradictory information. The number density scaling # factor might or might not have been provided. Have the # number density scaling factor default to the total number # of neutrals and ions across all elements and isotopes, if # it was not provided. Then go ahead and calculate the # abundances based on that. However, we need to be careful # that the abundances are not overwritten during the # instantiation of the class. if inputs_have_quantities: if np.isnan(self.n0): new_n = 0 * u.m ** -3 for key in _elements_and_isotopes: new_n += n_elems[key] self.n0 = new_n new_abundances = {} for key in _elements_and_isotopes: new_abundances[key] = np.float(n_elems[key] / self.n0) self._pars["abundances"] = new_abundances elif isinstance(inputs, (list, tuple)): try: _particle_instances = [Particle(particle) for particle in inputs] except (InvalidParticleError, TypeError) as exc: raise ParticleError( "Invalid inputs to IonizationStateCollection." ) from exc _particle_instances.sort(key=_atomic_number_and_mass_number) _elements_and_isotopes = [ particle.symbol for particle in _particle_instances ] new_ionic_fractions = { particle.symbol: np.full( particle.atomic_number + 1, fill_value=np.nan, dtype=np.float64 ) for particle in _particle_instances } else: raise TypeError("Incorrect inputs to set ionic_fractions.") for i in range(1, len(_particle_instances)): if _particle_instances[i - 1].element == _particle_instances[i].element: if ( not _particle_instances[i - 1].isotope and _particle_instances[i].isotope ): raise ParticleError( "Cannot have an element and isotopes of that element." ) self._particle_instances = _particle_instances self._base_particles = _elements_and_isotopes self._ionic_fractions = new_ionic_fractions def normalize(self) -> None: """ Normalize the ionic fractions so that the sum for each element equals one. """ for particle in self.base_particles: tot = np.sum(self.ionic_fractions[particle]) self.ionic_fractions[particle] = self.ionic_fractions[particle] / tot @property @validate_quantities def n_e(self) -> u.m ** -3: """ Return the electron number density under the assumption of quasineutrality. """ number_densities = self.number_densities n_e = 0.0 * u.m ** -3 for elem in self.base_particles: atomic_numb = atomic_number(elem) number_of_ionization_states = atomic_numb + 1 integer_charges = np.linspace(0, atomic_numb, number_of_ionization_states) n_e += np.sum(number_densities[elem] * integer_charges) return n_e @property @validate_quantities def n0(self) -> u.m ** -3: """Return the number density scaling factor.""" return self._pars["n"] @n0.setter @validate_quantities def n0(self, n: u.m ** -3): """Set the number density scaling factor.""" try: n = n.to(u.m ** -3) except u.UnitConversionError as exc: raise ParticleError("Units cannot be converted to u.m ** -3.") from exc except Exception as exc: raise ParticleError(f"{n} is not a valid number density.") from exc if n < 0 * u.m ** -3: raise ParticleError("Number density cannot be negative.") self._pars["n"] = n.to(u.m ** -3) @property def number_densities(self) -> Dict[str, u.Quantity]: """ Return a `dict` containing the number densities for element or isotope. """ return { elem: self.n0 * self.abundances[elem] * self.ionic_fractions[elem] for elem in self.base_particles } @property def abundances(self) -> Optional[Dict[ParticleLike, Real]]: """Return the elemental abundances.""" return self._pars["abundances"] @abundances.setter def abundances(self, abundances_dict: Optional[Dict[ParticleLike, Real]]): """ Set the elemental (or isotopic) abundances. The elements and isotopes must be the same as or a superset of the elements whose ionization states are being tracked. """ if abundances_dict is None: self._pars["abundances"] = {elem: np.nan for elem in self.base_particles} elif not isinstance(abundances_dict, dict): raise TypeError( f"The abundances attribute must be a dict with " f"elements or isotopes as keys and real numbers " f"representing relative abundances as values." ) else: old_keys = abundances_dict.keys() try: new_keys_dict = {} for old_key in old_keys: new_keys_dict[particle_symbol(old_key)] = old_key except Exception: raise ParticleError( f"The key {repr(old_key)} in the abundances " f"dictionary is not a valid element or isotope." ) new_elements = new_keys_dict.keys() old_elements_set = set(self.base_particles) new_elements_set = set(new_elements) if old_elements_set - new_elements_set: raise ParticleError( f"The abundances of the following particles are " f"missing: {old_elements_set - new_elements_set}" ) new_abundances_dict = {} for element in new_elements: inputted_abundance = abundances_dict[new_keys_dict[element]] try: inputted_abundance = float(inputted_abundance) except Exception: raise TypeError( f"The abundance for {element} was provided as" f"{inputted_abundance}, which cannot be " f"converted to a real number." ) from None if inputted_abundance < 0: raise ParticleError(f"The abundance of {element} is negative.") new_abundances_dict[element] = inputted_abundance self._pars["abundances"] = new_abundances_dict @property def log_abundances(self) -> Dict[str, Real]: """ Return a `dict` with atomic or isotope symbols as keys and the base 10 logarithms of the relative abundances as the corresponding values. """ log_abundances_dict = {} for key in self.abundances.keys(): log_abundances_dict[key] = np.log10(self.abundances[key]) return log_abundances_dict @log_abundances.setter def log_abundances(self, value: Optional[Dict[str, Real]]): """ Set the base 10 logarithm of the relative abundances. """ if value is not None: try: new_abundances_input = {} for key in value.keys(): new_abundances_input[key] = 10 ** value[key] self.abundances = new_abundances_input except Exception: raise ParticleError("Invalid log_abundances.") from None @property @validate_quantities(equivalencies=u.temperature_energy()) def T_e(self) -> u.K: """Return the electron temperature.""" return self._pars["T_e"] @T_e.setter @validate_quantities(equivalencies=u.temperature_energy()) def T_e(self, electron_temperature: u.K): """Set the electron temperature.""" try: temperature = electron_temperature.to( u.K, equivalencies=u.temperature_energy() ) except (AttributeError, u.UnitsError): raise ParticleError( f"{electron_temperature} is not a valid temperature." ) from None if temperature < 0 * u.K: raise ParticleError("The electron temperature cannot be negative.") self._pars["T_e"] = temperature @property def kappa(self) -> np.real: """ Return the kappa parameter for a kappa distribution function for electrons. The value of ``kappa`` must be greater than ``1.5`` in order to have a valid distribution function. If ``kappa`` equals `~numpy.inf`, then the distribution function reduces to a Maxwellian. """ return self._pars["kappa"] @kappa.setter def kappa(self, value: Real): """ Set the kappa parameter for a kappa distribution function for electrons. The value must be between ``1.5`` and `~numpy.inf`. """ kappa_errmsg = "kappa must be a real number greater than 1.5" if not isinstance(value, Real): raise TypeError(kappa_errmsg) if value <= 1.5: raise ValueError(kappa_errmsg) self._pars["kappa"] = np.real(value) @property def base_particles(self) -> List[str]: """ Return a list of the elements and isotopes whose ionization states are being kept track of. """ return self._base_particles @property def tol(self) -> np.real: """Return the absolute tolerance for comparisons.""" return self._tol @tol.setter def tol(self, atol: Real): """ Set the absolute tolerance for comparisons. """ if not isinstance(atol, Real): raise TypeError("The attribute tol must be a real number.") if 0 <= atol <= 1.0: self._tol = np.real(atol) else: raise ValueError("Need 0 <= tol <= 1.") def summarize(self, minimum_ionic_fraction: Real = 0.01) -> None: """ Print quicklook information for an `~plasmapy.particles.IonizationStateCollection` instance. Parameters ---------- minimum_ionic_fraction: `Real` If the ionic fraction for a particular ionization state is below this level, then information for it will not be printed. Defaults to 0.01. Examples -------- >>> states = IonizationStateCollection( ... {'H': [0.1, 0.9], 'He': [0.95, 0.05, 0.0]}, ... T_e = 12000 * u.K, ... n0 = 3e9 * u.cm ** -3, ... abundances = {'H': 1.0, 'He': 0.1}, ... kappa = 3.4, ... ) >>> states.summarize() IonizationStateCollection instance for: H, He ---------------------------------------------------------------- H 0+: 0.100 n_i = 3.00e+14 m**-3 H 1+: 0.900 n_i = 2.70e+15 m**-3 ---------------------------------------------------------------- He 0+: 0.950 n_i = 2.85e+14 m**-3 He 1+: 0.050 n_i = 1.50e+13 m**-3 ---------------------------------------------------------------- n_e = 2.71e+15 m**-3 T_e = 1.20e+04 K kappa = 3.40 ---------------------------------------------------------------- """ separator_line = 64 * "-" output = [] output.append( f"IonizationStateCollection instance for: {', '.join(self.base_particles)}" ) # Get the ionic symbol with the corresponding ionic fraction and # number density (if available), but only for the most abundant # ionization levels for each element. for ionization_state in self: states_info = ionization_state._get_states_info(minimum_ionic_fraction) if len(states_info) > 0: output += states_info output[-1] += "\n" + separator_line attributes = [] if np.isfinite(self.n_e): attributes.append("n_e = " + "{:.2e}".format(self.n_e.value) + " m**-3") if np.isfinite(self.T_e): attributes.append("T_e = " + "{:.2e}".format(self.T_e.value) + " K") if np.isfinite(self.kappa): attributes.append("kappa = " + "{:.2f}".format(self.kappa)) if attributes: attributes.append(separator_line) output.append("\n".join(attributes)) if len(output) > 1: output[0] += "\n" + separator_line output_string = "\n".join(output) else: output_string = output[0] print(output_string.strip("\n"))
def Maxwellian_1D(v, T, particle="e", V_drift=0, vTh=np.nan, units="units"): r""" Returns the probability at the velocity `v` in m/s to find a particle `particle` in a plasma of temperature `T` following the Maxwellian distribution function. Parameters ---------- v: ~astropy.units.Quantity The velocity in units convertible to m/s. T: ~astropy.units.Quantity The temperature in Kelvin. particle: str, optional Representation of the particle species(e.g., `'p'` for protons, `'D+'` for deuterium, or `'He-4 +1'` for :math:`He_4^{+1}` (singly ionized helium-4), which defaults to electrons. V_drift: ~astropy.units.Quantity, optional The drift velocity in units convertible to m/s. vTh: ~astropy.units.Quantity, optional Thermal velocity (most probable) in m/s. This is used for optimization purposes to avoid re-calculating vTh, for example when integrating over velocity-space. units: str, optional Selects whether to run function with units and unit checks (when equal to "units") or to run as unitless (when equal to "unitless"). The unitless version is substantially faster for intensive computations. Returns ------- f : ~astropy.units.Quantity Probability in Velocity^-1, normalized so that :math:`\int_{-\infty}^{+\infty} f(v) dv = 1`. Raises ------ TypeError The parameter arguments are not Quantities and cannot be converted into Quantities. ~astropy.units.UnitConversionError If the parameters are not in appropriate units. ValueError If the temperature is negative, or the particle mass or charge state cannot be found. Notes ----- In one dimension, the Maxwellian distribution function for a particle of mass m, velocity v, a drift velocity V and with temperature T is: .. math:: f = \sqrt{\frac{m}{2 \pi k_B T}} e^{-\frac{m}{2 k_B T} (v-V)^2} f = (\pi * v_Th^2)^{-1/2} e^{-(v - v_{drift})^2 / v_Th^2} where :math:`v_Th = \sqrt(2 k_B T / m)` is the thermal speed Examples -------- >>> from plasmapy.physics import Maxwellian_1D >>> from astropy import units as u >>> v=1*u.m/u.s >>> Maxwellian_1D(v=v, T= 30000*u.K, particle='e',V_drift=0*u.m/u.s) <Quantity 5.91632969e-07 s / m> """ if units == "units": # unit checks and conversions # checking velocity units v = v.to(u.m / u.s) # catching case where drift velocities have default values, they # need to be assigned units if V_drift == 0: if not isinstance(V_drift, astropy.units.quantity.Quantity): V_drift = V_drift * u.m / u.s # checking units of drift velocities V_drift = V_drift.to(u.m / u.s) # convert temperature to Kelvins T = T.to(u.K, equivalencies=u.temperature_energy()) if np.isnan(vTh): # get thermal velocity and thermal velocity squared vTh = (thermal_speed(T, particle=particle, method="most_probable")) elif not np.isnan(vTh): # check units of thermal velocity vTh = vTh.to(u.m / u.s) elif np.isnan(vTh) and units == "unitless": # assuming unitless temperature is in Kelvins vTh = (thermal_speed(T * u.K, particle=particle, method="most_probable")).si.value # Get thermal velocity squared vThSq = vTh**2 # Get square of relative particle velocity vSq = (v - V_drift)**2 # calculating distribution function coeff = (vThSq * np.pi)**(-1 / 2) expTerm = np.exp(-vSq / vThSq) distFunc = coeff * expTerm if units == "units": return distFunc.to(u.s / u.m) elif units == "unitless": return distFunc
def kappa_velocity_1D(v, T, kappa, particle="e", V_drift=0, vTh=np.nan, units="units"): r""" Return the probability at the velocity `v` in m/s to find a particle `particle` in a plasma of temperature `T` following the Kappa distribution function. The slope of the tail of the Kappa distribution function is set by 'kappa', which must be greater than :math:`1/2`. Parameters ---------- v: ~astropy.units.Quantity The velocity in units convertible to m/s. T: ~astropy.units.Quantity The temperature in Kelvin. kappa: ~astropy.units.Quantity The kappa parameter is a dimensionless number which sets the slope of the energy spectrum of suprathermal particles forming the tail of the Kappa velocity distribution function. Kappa must be greater than :math:`3/2`. particle: str, optional Representation of the particle species(e.g., `'p` for protons, `'D+'` for deuterium, or `'He-4 +1'` for :math:`He_4^{+1}` (singly ionized helium-4), which defaults to electrons. V_drift: ~astropy.units.Quantity, optional The drift velocity in units convertible to m/s. vTh: ~astropy.units.Quantity, optional Thermal velocity (most probable) in m/s. This is used for optimization purposes to avoid re-calculating `vTh`, for example when integrating over velocity-space. units: str, optional Selects whether to run function with units and unit checks (when equal to "units") or to run as unitless (when equal to "unitless"). The unitless version is substantially faster for intensive computations. Returns ------- f : ~astropy.units.Quantity probability in Velocity^-1, normalized so that :math:`\int_{-\infty}^{+\infty} f(v) dv = 1`. Raises ------ TypeError A parameter argument is not a `~astropy.units.Quantity` and cannot be converted into a `~astropy.units.Quantity`. ~astropy.units.UnitConversionError If the parameters is not in appropriate units. ValueError If the temperature is negative, or the particle mass or charge state cannot be found. Notes ----- In one dimension, the Kappa velocity distribution function describing the distribution of particles with speed :math:`v` in a plasma with temperature :math:`T` and suprathermal parameter :math:`\kappa` is given by: .. math:: f = A_\kappa \left(1 + \frac{(\vec{v} - \vec{V_{drift}})^2}{\kappa v_Th,\kappa^2}\right)^{-\kappa} where :math:`v_Th,\kappa` is the kappa thermal speed and :math:`A_\kappa = \frac{1}{\sqrt{\pi} \kappa^{3/2} v_Th,\kappa^2 \frac{\Gamma(\kappa + 1)}{\Gamma(\kappa - 1/2)}}` is the normalization constant. As :math:`\kappa` approaches infinity, the kappa distribution function converges to the Maxwellian distribution function. Examples -------- >>> from plasmapy.physics import kappa_velocity_1D >>> from astropy import units as u >>> v=1*u.m/u.s >>> kappa_velocity_1D(v=v, T=30000*u.K, kappa=4, particle='e',V_drift=0*u.m/u.s) <Quantity 6.75549854e-07 s / m> """ # must have kappa > 3/2 for distribution function to be valid if kappa <= 3 / 2: raise ValueError(f"Must have kappa > 3/2, instead of {kappa}.") if units == "units": # unit checks and conversions # checking velocity units v = v.to(u.m / u.s) # catching case where drift velocities have default values, they # need to be assigned units if V_drift == 0: if not isinstance(V_drift, astropy.units.quantity.Quantity): V_drift = V_drift * u.m / u.s # checking units of drift velocities V_drift = V_drift.to(u.m / u.s) # convert temperature to Kelvins T = T.to(u.K, equivalencies=u.temperature_energy()) if np.isnan(vTh): # get thermal velocity and thermal velocity squared vTh = kappa_thermal_speed(T, kappa, particle=particle) elif not np.isnan(vTh): # check units of thermal velocity vTh = vTh.to(u.m / u.s) elif np.isnan(vTh) and units == "unitless": # assuming unitless temperature is in Kelvins vTh = (kappa_thermal_speed(T * u.K, kappa, particle=particle)).si.value # Get thermal velocity squared and accounting for 1D instead of 3D vThSq = vTh**2 # Get square of relative particle velocity vSq = (v - V_drift)**2 # calculating distribution function expTerm = (1 + vSq / (kappa * vThSq))**(-kappa) coeff1 = 1 / (np.sqrt(np.pi) * kappa**(3 / 2) * vTh) coeff2 = gamma(kappa + 1) / (gamma(kappa - 1 / 2)) distFunc = coeff1 * coeff2 * expTerm if units == "units": return distFunc.to(u.s / u.m) elif units == "unitless": return distFunc
def Maxwellian_speed_1D(v, T, particle="e", V_drift=0, vTh=np.nan, units="units"): r""" Return the probability of finding a particle with speed `v` in m/s in an equilibrium plasma of temperature `T` which follows the Maxwellian distribution function. Parameters ---------- v: ~astropy.units.Quantity The speed in units convertible to m/s. T: ~astropy.units.Quantity The temperature, preferably in Kelvin. particle: str, optional Representation of the particle species(e.g., `'p'` for protons, `'D+'` for deuterium, or `'He-4 +1'` for :math:`He_4^{+1}` (singly ionized helium-4), which defaults to electrons. V_drift: ~astropy.units.Quantity The drift speed in units convertible to m/s. vTh: ~astropy.units.Quantity, optional Thermal velocity (most probable) in m/s. This is used for optimization purposes to avoid re-calculating vTh, for example when integrating over velocity-space. units: str, optional Selects whether to run function with units and unit checks (when equal to "units") or to run as unitless (when equal to "unitless"). The unitless version is substantially faster for intensive computations. Returns ------- f : ~astropy.units.Quantity Probability in speed^-1, normalized so that :math:`\int_{0}^{\infty} f(v) dv = 1`. Raises ------ TypeError The parameter arguments are not Quantities and cannot be converted into Quantities. ~astropy.units.UnitConversionError If the parameters is not in appropriate units. ValueError If the temperature is negative, or the particle mass or charge state cannot be found. Notes ----- In one dimension, the Maxwellian speed distribution function describing the distribution of particles with speed v in a plasma with temperature T is given by: .. math:: f(v) = 4 \pi v^2 (\pi * v_Th^2)^{-3/2} \exp(-(v - V_{drift})^2 / v_Th^2) where :math:`v_Th = \sqrt(2 k_B T / m)` is the thermal speed. Example ------- >>> from plasmapy.physics import Maxwellian_speed_1D >>> from astropy import units as u >>> v=1*u.m/u.s >>> Maxwellian_speed_1D(v=v, T= 30000*u.K, particle='e',V_drift=0*u.m/u.s) <Quantity 2.60235754e-18 s / m> """ if units == "units": # unit checks and conversions # checking velocity units v = v.to(u.m / u.s) # catching case where drift velocity has default value, and # needs to be assigned units if V_drift == 0: if not isinstance(V_drift, astropy.units.quantity.Quantity): V_drift = V_drift * u.m / u.s # checking drift velocity units V_drift = V_drift.to(u.m / u.s) # convert temperature to Kelvins T = T.to(u.K, equivalencies=u.temperature_energy()) if np.isnan(vTh): # get thermal velocity and thermal velocity squared vTh = (thermal_speed(T, particle=particle, method="most_probable")) elif not np.isnan(vTh): # check units of thermal velocity vTh = vTh.to(u.m / u.s) elif np.isnan(vTh) and units == "unitless": # assuming unitless temperature is in Kelvins vTh = (thermal_speed(T * u.K, particle=particle, method="most_probable")).si.value # getting square of thermal speed vThSq = vTh**2 # get square of relative particle speed vSq = (v - V_drift)**2 # calculating distribution function coeff1 = (np.pi * vThSq)**(-3 / 2) coeff2 = 4 * np.pi * vSq expTerm = np.exp(-vSq / vThSq) distFunc = coeff1 * coeff2 * expTerm if units == "units": return distFunc.to(u.s / u.m) elif units == "unitless": return distFunc
def collision_rate_electron_ion(T_e, n_e, ion_particle, coulomb_log=None, V=None, coulomb_log_method="classical"): r""" Momentum relaxation electron-ion collision rate From [3]_, equations (2.17) and (2.120) Considering a Maxwellian distribution of "test" electrons colliding with a Maxwellian distribution of "field" ions. This result is an electron momentum relaxation rate, and is used in many classical transport expressions. It is equivalent to: * 1/tau_e from ref [1]_ eqn (1) pp. #, * 1/tau_e from ref [2]_ eqn (1) pp. #, * nu_e\i_S from ref [2]_ eqn (1) pp. #, Parameters ---------- T_e : ~astropy.units.Quantity The electron temperature of the Maxwellian test electrons n_e : ~astropy.units.Quantity The number density of the Maxwellian test electrons ion_particle: str String signifying a particle type of the field ions, including charge state information. V : ~astropy.units.Quantity, optional The relative velocity between particles. If not provided, thermal velocity is assumed: :math:`\mu V^2 \sim 2 k_B T` where `mu` is the reduced mass. coulomb_log : float or dimensionless ~astropy.units.Quantity, optional Option to specify a Coulomb logarithm of the electrons on the ions. If not specified, the Coulomb log will is calculated using the `~plasmapy.physics.transport.Coulomb_logarithm` function. coulomb_log_method : string, optional Method used for Coulomb logarithm calculation (see that function for more documentation). Choose from "classical" or "GMS-1" to "GMS-6". References ---------- .. [1] Braginskii .. [2] Formulary .. [3] Callen Chapter 2, http://homepages.cae.wisc.edu/~callen/chap2.pdf Examples -------- >>> from astropy import units as u >>> collision_rate_electron_ion(0.1*u.eV, 1e6/u.m**3, 'p') <Quantity 0.00180172 1 / s> >>> collision_rate_electron_ion(100*u.eV, 1e6/u.m**3, 'p') <Quantity 8.6204672e-08 1 / s> >>> collision_rate_electron_ion(100*u.eV, 1e20/u.m**3, 'p') <Quantity 3936037.8595928 1 / s> >>> collision_rate_electron_ion(100*u.eV, 1e20/u.m**3, 'p', coulomb_log_method = 'GMS-1') <Quantity 3872922.52743562 1 / s> >>> collision_rate_electron_ion(0.1*u.eV, 1e6/u.m**3, 'p', V = c/100) <Quantity 4.41166015e-07 1 / s> >>> collision_rate_electron_ion(100*u.eV, 1e20/u.m**3, 'p', coulomb_log = 20) <Quantity 5812633.74935004 1 / s> """ from plasmapy.physics.transport.collisions import Coulomb_logarithm T_e = T_e.to(u.K, equivalencies=u.temperature_energy()) if V is not None: V = V else: # electron thermal velocity (most probable) V = np.sqrt(2 * k_B * T_e / m_e) if coulomb_log is not None: coulomb_log_val = coulomb_log else: particles = ['e', ion_particle] coulomb_log_val = Coulomb_logarithm(T_e, n_e, particles, V, method=coulomb_log_method) # this is the same as b_perp in collisions.py, using most probable thermal velocity for V # and using ion mass instead of reduced mass bperp = e**2 / (4 * np.pi * eps0 * m_e * V**2) # collisional cross-section sigma = np.pi * (2 * bperp)**2 # collisional frequency with Coulomb logarithm to correct for small angle collisions nu = n_e * sigma * V * coulomb_log_val # this coefficient is the constant that pops out when comparing this definition of # collisional frequency to the one in collisions.py coeff = 4 / np.sqrt(np.pi) / 3 # collisional frequency modified by the constant difference nu_e = coeff * nu return nu_e.to(1 / u.s)
) return np.min(probe_characteristic.current) @validate_quantities( ion_saturation_current={ "can_be_negative": True, "can_be_inf": False, "can_be_nan": False, }, T_e={ "can_be_negative": False, "can_be_inf": False, "can_be_nan": False, "equivalencies": u.temperature_energy(), }, probe_area={"can_be_negative": False, "can_be_inf": False, "can_be_nan": False}, validations_on_return={"can_be_negative": False}, ) def get_ion_density_LM( ion_saturation_current: u.A, T_e: u.eV, probe_area: u.m ** 2, gas ) -> u.m ** -3: r"""Implement the Langmuir-Mottley (LM) method of obtaining the ion density. Parameters ---------- ion_saturation_current : ~astropy.units.Quantity The ion saturation current in units convertible to A.
def T_e(self) -> u.K: """Return the electron temperature.""" if self._T_e is None: raise AtomicError("No electron temperature has been specified.") return self._T_e.to(u.K, equivalencies=u.temperature_energy())
class IonizationState: """ Representation of the ionization state distribution of a single element or isotope. Parameters ---------- particle: str, integer, or ~plasmapy.particles.Particle A `str` or `~plasmapy.particles.Particle` instance representing an element or isotope, or an integer representing the atomic number of an element. ionic_fractions: ~numpy.ndarray, list, tuple, or ~astropy.units.Quantity; optional The ionization fractions of an element, where the indices correspond to integer charge. This argument should contain the atomic number plus one items, and must sum to one within an absolute tolerance of ``tol`` if dimensionless. Alternatively, this argument may be a `~astropy.units.Quantity` that represents the number densities of each neutral/ion. T_e: ~astropy.units.Quantity, keyword-only, optional The electron temperature or thermal energy per particle. n_elem: ~astropy.units.Quantity, keyword-only, optional The number density of the element, including neutrals and all ions. tol: float or integer, keyword-only, optional The absolute tolerance used by `~numpy.isclose` when testing normalizations and making comparisons. Defaults to ``1e-15``. Raises ------ ~plasmapy.utils.AtomicError If the ionic fractions are not normalized or contain invalid values, or if number density information is provided through both ``ionic_fractions`` and ``n_elem``. ~plasmapy.utils.InvalidParticleError If the particle is invalid. Examples -------- >>> states = IonizationState('H', [0.6, 0.4], n_elem=1*u.cm**-3, T_e=11000*u.K) >>> states.ionic_fractions[0] # fraction of hydrogen that is neutral 0.6 >>> states.ionic_fractions[1] # fraction of hydrogen that is ionized 0.4 >>> states.n_e # electron number density <Quantity 400000. 1 / m3> >>> states.n_elem # element number density <Quantity 1000000. 1 / m3> Notes ----- Calculation of collisional ionization equilibrium has not yet been implemented. """ # TODO: Allow this class to (optionally?) handle negatively charged # TODO: ions. There are instances where singly negatively charged # TODO: ions are important in astrophysical plasmas, such as H- in # TODO: the atmospheres of relatively cool stars. There may be some # TODO: rare situations where doubly negatively charged ions show up # TODO: too, but triply negatively charged ions are very unlikely. # TODO: Add in functionality to find equilibrium ionization states. @validate_quantities(T_e={"equivalencies": u.temperature_energy()}) @particle_input(require="element", exclude="ion") def __init__( self, particle: Particle, ionic_fractions=None, *, T_e: u.K = np.nan * u.K, kappa: Real = np.inf, n_elem: u.m**-3 = np.nan * u.m**-3, tol: Union[float, int] = 1e-15, ): """Initialize an `~plasmapy.particles.IonizationState` instance.""" self._particle_instance = particle try: self.tol = tol self.T_e = T_e self.kappa = kappa if (not np.isnan(n_elem) and isinstance(ionic_fractions, u.Quantity) and ionic_fractions.si.unit == u.m**-3): raise AtomicError( "Cannot simultaneously provide number density " "through both n_elem and ionic_fractions.") self.n_elem = n_elem self.ionic_fractions = ionic_fractions if ionic_fractions is None and not np.isnan(self.T_e): warnings.warn( "Collisional ionization equilibration has not yet " "been implemented in IonizationState; cannot set " "ionic fractions.") except Exception as exc: raise AtomicError(f"Unable to create IonizationState instance for " f"{particle.particle}.") from exc def __str__(self) -> str: return f"<IonizationState instance for {self.base_particle}>" def __repr__(self) -> str: return self.__str__() def __getitem__(self, value) -> State: """Return information for a single ionization level.""" if isinstance(value, slice): raise TypeError("IonizationState instances cannot be sliced.") if isinstance(value, Integral) and 0 <= value <= self.atomic_number: result = State( value, self.ionic_fractions[value], self.ionic_symbols[value], self.number_densities[value], ) else: if not isinstance(value, Particle): try: value = Particle(value) except InvalidParticleError as exc: raise InvalidParticleError( f"{value} is not a valid integer charge or " f"particle.") from exc same_element = value.element == self.element same_isotope = value.isotope == self.isotope has_charge_info = value.is_category( any_of=["charged", "uncharged"]) if same_element and same_isotope and has_charge_info: Z = value.integer_charge result = State( Z, self.ionic_fractions[Z], self.ionic_symbols[Z], self.number_densities[Z], ) else: if not same_element or not same_isotope: raise AtomicError("Inconsistent element or isotope.") elif not has_charge_info: raise ChargeError("No integer charge provided.") return result def __setitem__(self, key, value): raise NotImplementedError( "Item assignment of an IonizationState instance is not " "allowed because the ionic fractions for different " "ionization levels must be set simultaneously due to the " "normalization constraint.") def __iter__(self): """Initialize an instance prior to iteration.""" self._charge_index = 0 return self def __next__(self): """ Return a `~plasmapy.particles.State` instance that contains information about a particular ionization level. """ if self._charge_index <= self.atomic_number: result = State( self._charge_index, self._ionic_fractions[self._charge_index], self.ionic_symbols[self._charge_index], self.number_densities[self._charge_index], ) self._charge_index += 1 return result else: del self._charge_index raise StopIteration def __eq__(self, other): """ Return `True` if the ionic fractions, number density scaling factor (if set), and electron temperature (if set) are all equal, and `False` otherwise. Raises ------ TypeError If ``other`` is not an `~plasmapy.particles.IonizationState` instance. AtomicError If ``other`` corresponds to a different element or isotope. Examples -------- >>> IonizationState('H', [1, 0], tol=1e-6) == IonizationState('H', [1, 1e-6], tol=1e-6) True >>> IonizationState('H', [1, 0], tol=1e-8) == IonizationState('H', [1, 1e-6], tol=1e-5) False """ if not isinstance(other, IonizationState): raise TypeError( "An instance of the IonizationState class may only be " "compared with another IonizationState instance.") same_element = self.element == other.element same_isotope = self.isotope == other.isotope if not same_element or not same_isotope: raise AtomicError( "An instance of the IonizationState class may only be " "compared with another IonizationState instance if " "both correspond to the same element and/or isotope.") # Use the tighter of the two tolerances. For thermodynamic # quantities, use it as a relative tolerance because the values # may substantially depart from order unity. min_tol = np.min([self.tol, other.tol]) same_T_e = (np.isnan(self.T_e) and np.isnan(other.T_e) or u.allclose( self.T_e, other.T_e, rtol=min_tol * u.K, atol=0 * u.K)) same_n_elem = (np.isnan(self.n_elem) and np.isnan(other.n_elem) or u.allclose(self.n_elem, other.n_elem, rtol=min_tol * u.m**-3, atol=0 * u.m**-3)) # For the next line, recall that np.nan == np.nan is False (sigh) same_fractions = np.any([ np.allclose(self.ionic_fractions, other.ionic_fractions, rtol=0, atol=min_tol), np.all(np.isnan(self.ionic_fractions)) and np.all(np.isnan(other.ionic_fractions)), ]) return np.all([ same_element, same_isotope, same_T_e, same_n_elem, same_fractions ]) @property def ionic_fractions(self) -> np.ndarray: """ Return the ionic fractions, where the index corresponds to the integer charge. Examples -------- >>> hydrogen_states = IonizationState('H', [0.9, 0.1]) >>> hydrogen_states.ionic_fractions array([0.9, 0.1]) """ return self._ionic_fractions @ionic_fractions.setter def ionic_fractions(self, fractions): """ Set the ionic fractions, while checking that the new values are valid and normalized to one. """ if fractions is None or np.all(np.isnan(fractions)): self._ionic_fractions = np.full(self.atomic_number + 1, np.nan, dtype=np.float64) return try: if np.min(fractions) < 0: raise AtomicError("Cannot have negative ionic fractions.") if len(fractions) != self.atomic_number + 1: raise AtomicError("The length of ionic_fractions must be " f"{self.atomic_number + 1}.") if isinstance(fractions, u.Quantity): fractions = fractions.to(u.m**-3) self.n_elem = np.sum(fractions) self._ionic_fractions = np.array(fractions / self.n_elem) else: fractions = np.array(fractions, dtype=np.float64) sum_of_fractions = np.sum(fractions) all_nans = np.all(np.isnan(fractions)) if not all_nans: if np.any(fractions < 0) or np.any(fractions > 1): raise AtomicError( "Ionic fractions must be between 0 and 1.") if not np.isclose( sum_of_fractions, 1, rtol=0, atol=self.tol): raise AtomicError("Ionic fractions must sum to one.") self._ionic_fractions = fractions except Exception as exc: raise AtomicError( f"Unable to set ionic fractions of {self.element} " f"to {fractions}.") from exc def _is_normalized(self, tol: Optional[Real] = None) -> bool: """ Return `True` if the sum of the ionization fractions is equal to one within the allowed tolerance, and `False` otherwise. """ tol = tol if tol is not None else self.tol if not isinstance(tol, Real): raise TypeError("tol must be an int or float.") if not 0 <= tol < 1: raise ValueError("Need 0 <= tol < 1.") total = np.sum(self._ionic_fractions) return np.isclose(total, 1, atol=tol, rtol=0) def normalize(self) -> None: """ Normalize the ionization state distribution (if set) so that the sum becomes equal to one. """ self._ionic_fractions = self._ionic_fractions / np.sum( self._ionic_fractions) @property def equil_ionic_fractions(self, T_e: u.K = None): """ Return the equilibrium ionic fractions for temperature ``T_e`` or the temperature set in the IonizationState instance. Not implemented. """ raise NotImplementedError @validate_quantities(equivalencies=u.temperature_energy()) def equilibrate(self, T_e: u.K = np.nan * u.K): """ Set the ionic fractions to collisional ionization equilibrium for temperature ``T_e``. Not implemented. """ # self.ionic_fractions = self.equil_ionic_fractions raise NotImplementedError @property @validate_quantities def n_e(self) -> u.m**-3: """ Return the electron number density assuming a single species plasma. """ return np.sum(self._n_elem * self.ionic_fractions * self.integer_charges) @property @validate_quantities def n_elem(self) -> u.m**-3: """Return the total number density of neutrals and all ions.""" return self._n_elem.to(u.m**-3) @n_elem.setter @validate_quantities def n_elem(self, value: u.m**-3): """Set the number density of neutrals and all ions.""" if value < 0 * u.m**-3: raise AtomicError if 0 * u.m**-3 < value <= np.inf * u.m**-3: self._n_elem = value.to(u.m**-3) elif np.isnan(value): self._n_elem = np.nan * u.m**-3 @property @validate_quantities def number_densities(self) -> u.m**-3: """Return the number densities for each state.""" try: return (self.n_elem * self.ionic_fractions).to(u.m**-3) except Exception: return np.full(self.atomic_number + 1, np.nan) * u.m**-3 @number_densities.setter @validate_quantities def number_densities(self, value: u.m**-3): """Set the number densities for each state.""" if np.any(value.value < 0): raise AtomicError("Number densities cannot be negative.") if len(value) != self.atomic_number + 1: raise AtomicError(f"Incorrect number of charge states for " f"{self.base_particle}") value = value.to(u.m**-3) self._n_elem = value.sum() self._ionic_fractions = value / self._n_elem @property @validate_quantities(equivalencies=u.temperature_energy()) def T_e(self) -> u.K: """Return the electron temperature.""" if self._T_e is None: raise AtomicError("No electron temperature has been specified.") return self._T_e.to(u.K, equivalencies=u.temperature_energy()) @T_e.setter @validate_quantities(equivalencies=u.temperature_energy()) def T_e(self, value: u.K): """Set the electron temperature.""" try: value = value.to(u.K, equivalencies=u.temperature_energy()) except (AttributeError, u.UnitsError, u.UnitConversionError): raise AtomicError("Invalid temperature.") from None else: if value < 0 * u.K: raise AtomicError("T_e cannot be negative.") self._T_e = value @property def kappa(self) -> np.real: """ Return the kappa parameter for a kappa distribution function for electrons. The value of ``kappa`` must be greater than ``1.5`` in order to have a valid distribution function. If ``kappa`` equals `~numpy.inf`, then the distribution function reduces to a Maxwellian. """ return self._kappa @kappa.setter def kappa(self, value: Real): """ Set the kappa parameter for a kappa distribution function for electrons. The value must be between ``1.5`` and `~numpy.inf`. """ kappa_errmsg = "kappa must be a real number greater than 1.5" if not isinstance(value, Real): raise TypeError(kappa_errmsg) if value <= 1.5: raise ValueError(kappa_errmsg) self._kappa = np.real(value) @property def element(self) -> str: """Return the atomic symbol of the element.""" return self._particle_instance.element @property def isotope(self) -> Optional[str]: """ Return the isotope symbol for an isotope, or `None` if the particle is not an isotope. """ return self._particle_instance.isotope @property def base_particle(self) -> str: """Return the symbol of the element or isotope.""" return self.isotope if self.isotope else self.element @property def atomic_number(self) -> int: """Return the atomic number of the element.""" return self._particle_instance.atomic_number @property def _particle_instances(self) -> List[Particle]: """ Return a list of the `~plasmapy.particles.Particle` class instances corresponding to each ion. """ return [ Particle(self._particle_instance.particle, Z=i) for i in range(self.atomic_number + 1) ] @property def ionic_symbols(self) -> List[str]: """Return the ionic symbols for all charge states.""" return [particle.ionic_symbol for particle in self._particle_instances] @property def integer_charges(self) -> np.ndarray: """Return an array with the integer charges.""" return np.arange(0, self.atomic_number + 1, dtype=np.int) @property def Z_mean(self) -> np.float64: """Return the mean integer charge""" if np.nan in self.ionic_fractions: raise ChargeError( "Z_mean cannot be found because no ionic fraction " f"information is available for {self.base_particle}.") return np.sum(self.ionic_fractions * np.arange(self.atomic_number + 1)) @property def Z_rms(self) -> np.float64: """Return the root mean square integer charge.""" return np.sqrt( np.sum(self.ionic_fractions * np.arange(self.atomic_number + 1)**2)) @property def Z_most_abundant(self) -> List[Integral]: """ Return a `list` of the integer charges with the highest ionic fractions. Examples -------- >>> He = IonizationState('He', [0.2, 0.5, 0.3]) >>> He.Z_most_abundant [1] >>> Li = IonizationState('Li', [0.4, 0.4, 0.2, 0.0]) >>> Li.Z_most_abundant [0, 1] """ if np.any(np.isnan(self.ionic_fractions)): raise AtomicError( f"Cannot find most abundant ion of {self.base_particle} " f"because the ionic fractions have not been defined.") return np.flatnonzero( self.ionic_fractions == self.ionic_fractions.max()).tolist() @property def tol(self) -> Real: """Return the absolute tolerance for comparisons.""" return self._tol @tol.setter def tol(self, atol: Real): """Set the absolute tolerance for comparisons.""" if not isinstance(atol, Real): raise TypeError("The attribute tol must be a real number.") if 0 <= atol < 1: self._tol = atol else: raise ValueError("Need 0 <= tol < 1.") def _get_states_info(self, minimum_ionic_fraction=0.01) -> List[str]: """ Return a `list` containing the ion symbol, ionic fraction, and (if available) the number density for that ion. """ states_info = [] for state in self: if state.ionic_fraction > minimum_ionic_fraction: state_info = "" symbol = state.ionic_symbol if state.integer_charge < 10: symbol = symbol[:-2] + " " + symbol[-2:] fraction = "{:.3f}".format(state.ionic_fraction) state_info += f"{symbol}: {fraction}" if np.isfinite(self.n_elem): value = "{:.2e}".format(state.number_density.si.value) state_info += f" n_i = {value} m**-3" states_info.append(state_info) return states_info def info(self, minimum_ionic_fraction: Real = 0.01) -> None: """ Print quicklook information for an `~plasmapy.particles.IonizationState` instance. Parameters ---------- minimum_ionic_fraction: Real If the ionic fraction for a particular ionization state is below this level, then information for it will not be printed. Defaults to 0.01. Example ------- >>> He_states = IonizationState( ... 'He', ... [0.941, 0.058, 0.001], ... T_e = 5.34 * u.K, ... kappa = 4.05, ... n_elem = 5.51e19 * u.m ** -3, ... ) >>> He_states.info() IonizationState instance for He with Z_mean = 0.06 ---------------------------------------------------------------- He 0+: 0.941 n_i = 5.18e+19 m**-3 He 1+: 0.058 n_i = 3.20e+18 m**-3 ---------------------------------------------------------------- n_elem = 5.51e+19 m**-3 n_e = 3.31e+18 m**-3 T_e = 5.34e+00 K kappa = 4.05 ---------------------------------------------------------------- """ separator_line = [64 * "-"] scientific = "{:.2e}" floaty = "{:.2f}" n_elem = scientific.format(self.n_elem.value) n_e = scientific.format(self.n_e.value) T_e = scientific.format(self.T_e.value) kappa = floaty.format(self.kappa) Z_mean = floaty.format(self.Z_mean) output = [ f"IonizationState instance for {self.base_particle} with Z_mean = {Z_mean}" ] attributes = [] if not np.all(np.isnan(self.ionic_fractions)): output += separator_line output += self._get_states_info(minimum_ionic_fraction) output += separator_line if not np.isnan(self.n_elem): attributes.append(f"n_elem = {n_elem} m**-3") attributes.append(f"n_e = {n_e} m**-3") if not np.isnan(self.T_e): attributes.append(f"T_e = {T_e} K") if np.isfinite(self.kappa): attributes.append(f"kappa = {kappa}") if attributes: attributes += separator_line output += attributes for line in output: print(line)
def kappa_velocity_1D(v, T, kappa, particle="e", V_drift=0, vTh=np.nan, units="units"): r""" Return the probability at the velocity `v` in m/s to find a particle `particle` in a plasma of temperature `T` following the Kappa distribution function. The slope of the tail of the Kappa distribution function is set by 'kappa', which must be greater than :math:`1/2`. Parameters ---------- v: ~astropy.units.Quantity The velocity in units convertible to m/s. T: ~astropy.units.Quantity The temperature in Kelvin. kappa: ~astropy.units.Quantity The kappa parameter is a dimensionless number which sets the slope of the energy spectrum of suprathermal particles forming the tail of the Kappa velocity distribution function. Kappa must be greater than :math:`3/2`. particle: str, optional Representation of the particle species(e.g., `'p` for protons, `'D+'` for deuterium, or `'He-4 +1'` for :math:`He_4^{+1}` (singly ionized helium-4), which defaults to electrons. V_drift: ~astropy.units.Quantity, optional The drift velocity in units convertible to m/s. vTh: ~astropy.units.Quantity, optional Thermal velocity (most probable) in m/s. This is used for optimization purposes to avoid re-calculating `vTh`, for example when integrating over velocity-space. units: str, optional Selects whether to run function with units and unit checks (when equal to "units") or to run as unitless (when equal to "unitless"). The unitless version is substantially faster for intensive computations. Returns ------- f : ~astropy.units.Quantity probability in Velocity^-1, normalized so that :math:`\int_{-\infty}^{+\infty} f(v) dv = 1`. Raises ------ TypeError A parameter argument is not a `~astropy.units.Quantity` and cannot be converted into a `~astropy.units.Quantity`. ~astropy.units.UnitConversionError If the parameters is not in appropriate units. ValueError If the temperature is negative, or the particle mass or charge state cannot be found. Notes ----- In one dimension, the Kappa velocity distribution function describing the distribution of particles with speed :math:`v` in a plasma with temperature :math:`T` and suprathermal parameter :math:`\kappa` is given by: .. math:: f = A_\kappa \left(1 + \frac{(\vec{v} - \vec{V_{drift}})^2}{\kappa v_Th,\kappa^2}\right)^{-\kappa} where :math:`v_Th,\kappa` is the kappa thermal speed and :math:`A_\kappa = \frac{1}{\sqrt{\pi} \kappa^{3/2} v_Th,\kappa^2 \frac{\Gamma(\kappa + 1)}{\Gamma(\kappa - 1/2)}}` is the normalization constant. As :math:`\kappa` approaches infinity, the kappa distribution function converges to the Maxwellian distribution function. Examples -------- >>> from plasmapy.physics import kappa_velocity_1D >>> from astropy import units as u >>> v=1*u.m/u.s >>> kappa_velocity_1D(v=v, T=30000*u.K, kappa=4, particle='e',V_drift=0*u.m/u.s) <Quantity 6.75549854e-07 s / m> """ # must have kappa > 3/2 for distribution function to be valid if kappa <= 3 / 2: raise ValueError(f"Must have kappa > 3/2, instead of {kappa}.") if units == "units": # unit checks and conversions # checking velocity units v = v.to(u.m / u.s) # catching case where drift velocities have default values, they # need to be assigned units if V_drift == 0: if not isinstance(V_drift, astropy.units.quantity.Quantity): V_drift = V_drift * u.m / u.s # checking units of drift velocities V_drift = V_drift.to(u.m / u.s) # convert temperature to Kelvins T = T.to(u.K, equivalencies=u.temperature_energy()) if np.isnan(vTh): # get thermal velocity and thermal velocity squared vTh = kappa_thermal_speed(T, kappa, particle=particle) elif not np.isnan(vTh): # check units of thermal velocity vTh = vTh.to(u.m / u.s) elif np.isnan(vTh) and units == "unitless": # assuming unitless temperature is in Kelvins vTh = (kappa_thermal_speed(T * u.K, kappa, particle=particle)).si.value # Get thermal velocity squared and accounting for 1D instead of 3D vThSq = vTh ** 2 # Get square of relative particle velocity vSq = (v - V_drift) ** 2 # calculating distribution function expTerm = (1 + vSq / (kappa * vThSq)) ** (-kappa) coeff1 = 1 / (np.sqrt(np.pi) * kappa ** (3 / 2) * vTh) coeff2 = gamma(kappa + 1) / (gamma(kappa - 1 / 2)) distFunc = coeff1 * coeff2 * expTerm if units == "units": return distFunc.to(u.s / u.m) elif units == "unitless": return distFunc
def kappa_velocity_3D(vx, vy, vz, T, kappa, particle="e", Vx_drift=0, Vy_drift=0, Vz_drift=0, vTh=np.nan, units="units"): r""" Return the probability of finding a particle with velocity components `v_x`, `v_y`, and `v_z`in m/s in a suprathermal plasma of temperature `T` and parameter 'kappa' which follows the 3D Kappa distribution function. This function assumes Cartesian coordinates. Parameters ---------- vx: ~astropy.units.Quantity The velocity in x-direction units convertible to m/s. vy: ~astropy.units.Quantity The velocity in y-direction units convertible to m/s. vz: ~astropy.units.Quantity The velocity in z-direction units convertible to m/s. T: ~astropy.units.Quantity The temperature, preferably in Kelvin. kappa: ~astropy.units.Quantity The kappa parameter is a dimensionless number which sets the slope of the energy spectrum of suprathermal particles forming the tail of the Kappa velocity distribution function. Kappa must be greater than :math:`3/2`. particle: str, optional Representation of the particle species(e.g., 'p' for protons, 'D+' for deuterium, or 'He-4 +1' for :math:`He_4^{+1}` : singly ionized helium-4), which defaults to electrons. Vx_drift: ~astropy.units.Quantity, optional The drift velocity in x-direction units convertible to m/s. Vy_drift: ~astropy.units.Quantity, optional The drift velocity in y-direction units convertible to m/s. Vz_drift: ~astropy.units.Quantity, optional The drift velocity in z-direction units convertible to m/s. vTh: ~astropy.units.Quantity, optional Thermal velocity (most probable) in m/s. This is used for optimization purposes to avoid re-calculating `vTh`, for example when integrating over velocity-space. units: str, optional Selects whether to run function with units and unit checks (when equal to "units") or to run as unitless (when equal to "unitless"). The unitless version is substantially faster for intensive computations. Returns ------- f : ~astropy.units.Quantity probability in Velocity^-1, normalized so that: :math:`\iiint_{0}^{\infty} f(\vec{v}) d\vec{v} = 1` Raises ------ TypeError The parameter arguments are not Quantities and cannot be converted into Quantities. ~astropy.units.UnitConversionError If the parameters is not in appropriate units. ValueError If the temperature is negative, or the particle mass or charge state cannot be found. Notes ----- In three dimensions, the Kappa velocity distribution function describing the distribution of particles with speed :math:`v` in a plasma with temperature :math:`T` and suprathermal parameter :math:`\kappa` is given by: .. math:: f = A_\kappa \left(1 + \frac{(\vec{v} - \vec{V_{drift}})^2}{\kappa v_Th,\kappa^2}\right)^{-(\kappa + 1)} where :math:`v_Th,\kappa` is the kappa thermal speed and :math:`A_\kappa = \frac{1}{2 \pi (\kappa v_Th,\kappa^2)^{3/2}} \frac{\Gamma(\kappa + 1)}{\Gamma(\kappa - 1/2) \Gamma(3/2)}` is the normalization constant. As :math:`\kappa` approaches infinity, the kappa distribution function converges to the Maxwellian distribution function. See also -------- kappa_velocity_1D kappa_thermal_speed Example ------- >>> from plasmapy.physics import kappa_velocity_3D >>> from astropy import units as u >>> v=1*u.m/u.s >>> kappa_velocity_3D(vx=v, ... vy=v, ... vz=v, ... T=30000*u.K, ... kappa=4, ... particle='e', ... Vx_drift=0*u.m/u.s, ... Vy_drift=0*u.m/u.s, ... Vz_drift=0*u.m/u.s) <Quantity 3.7833988e-19 s3 / m3> """ # must have kappa > 3/2 for distribution function to be valid if kappa <= 3 / 2: raise ValueError(f"Must have kappa > 3/2, instead of {kappa}.") if units == "units": # unit checks and conversions # checking velocity units vx = vx.to(u.m / u.s) vy = vy.to(u.m / u.s) vz = vz.to(u.m / u.s) # catching case where drift velocities have default values, they # need to be assigned units if Vx_drift == 0: if not isinstance(Vx_drift, astropy.units.quantity.Quantity): Vx_drift = Vx_drift * u.m / u.s if Vy_drift == 0: if not isinstance(Vy_drift, astropy.units.quantity.Quantity): Vy_drift = Vy_drift * u.m / u.s if Vz_drift == 0: if not isinstance(Vz_drift, astropy.units.quantity.Quantity): Vz_drift = Vz_drift * u.m / u.s # checking units of drift velocities Vx_drift = Vx_drift.to(u.m / u.s) Vy_drift = Vy_drift.to(u.m / u.s) Vz_drift = Vz_drift.to(u.m / u.s) # convert temperature to Kelvins T = T.to(u.K, equivalencies=u.temperature_energy()) if np.isnan(vTh): # get thermal velocity and thermal velocity squared vTh = kappa_thermal_speed(T, kappa, particle=particle) elif not np.isnan(vTh): # check units of thermal velocity vTh = vTh.to(u.m / u.s) elif np.isnan(vTh) and units == "unitless": # assuming unitless temperature is in Kelvins vTh = (kappa_thermal_speed(T * u.K, kappa, particle=particle)).si.value # getting square of thermal velocity vThSq = vTh ** 2 # Get square of relative particle velocity vSq = ((vx - Vx_drift) ** 2 + (vy - Vy_drift) ** 2 + (vz - Vz_drift) ** 2) # calculating distribution function expTerm = (1 + vSq / (kappa * vThSq)) ** (-(kappa + 1)) coeff1 = 1 / (2 * np.pi * (kappa * vThSq) ** (3 / 2)) coeff2 = gamma(kappa + 1) / (gamma(kappa - 1 / 2) * gamma(3 / 2)) distFunc = coeff1 * coeff2 * expTerm if units == "units": return distFunc.to((u.s / u.m)**3) elif units == "unitless": return distFunc
def Debye_length(T_e, n_e): r"""Calculate the characteristic decay length for electric fields, due to charge screening. Parameters ---------- T_e: ~astropy.units.Quantity Electron temperature n_e: ~astropy.units.Quantity Electron number density Returns ------- lambda_D : ~astropy.units.Quantity The Debye length in meters Raises ------ TypeError If either argument is not a `~astropy.units.Quantity` ~astropy.units.UnitConversionError If either argument is in incorrect units ValueError If either argument contains invalid values Warns ----- ~astropy.units.UnitsWarning If units are not provided, SI units are assumed Notes ----- The Debye length is the exponential scale length for charge screening and is given by .. math:: \lambda_D = \sqrt{\frac{\epsilon_0 k_b T_e}{n_e e^2}} for an electron plasma with nearly stationary ions. The electrical potential will drop by a factor of 1/e every Debye length. Plasmas will generally be quasineutral on length scales significantly larger than the Debye length. See Also -------- Debye_number Example ------- >>> from astropy import units as u >>> Debye_length(5e6*u.K, 5e15*u.m**-3) <Quantity 0.00218226 m> """ T_e = T_e.to(u.K, equivalencies=u.temperature_energy()) lambda_D = np.sqrt(eps0 * k_B * T_e / (n_e * e**2)) return lambda_D.to(u.m)
def thermal_speed(T, particle="e-", method="most_probable"): r""" Return the most probable speed for a particle within a Maxwellian distribution. Parameters ---------- T : ~astropy.units.Quantity The particle temperature in either kelvin or energy per particle particle : str, optional Representation of the particle species (e.g., `'p'` for protons, `'D+'` for deuterium, or `'He-4 +1'` for singly ionized helium-4), which defaults to electrons. If no charge state information is provided, then the particles are assumed to be singly charged. method : str, optional Method to be used for calculating the thermal speed. Options are `'most_probable'` (default), `'rms'`, and `'mean_magnitude'`. Returns ------- V : ~astropy.units.Quantity particle thermal speed Raises ------ TypeError The particle temperature is not a ~astropy.units.Quantity ~astropy.units.UnitConversionError If the particle temperature is not in units of temperature or energy per particle ValueError The particle temperature is invalid or particle cannot be used to identify an isotope or particle Warns ----- RelativityWarning If the ion sound speed exceeds 5% of the speed of light, or ~astropy.units.UnitsWarning If units are not provided, SI units are assumed. Notes ----- The particle thermal speed is given by: .. math:: V_{th,i} = \sqrt{\frac{2 k_B T_i}{m_i}} This function yields the most probable speed within a distribution function. However, the definition of thermal velocity varies by the square root of two depending on whether or not this velocity absorbs that factor in the expression for a Maxwellian distribution. In particular, the expression given in the NRL Plasma Formulary [1] is a square root of two smaller than the result from this function. Examples -------- >>> from astropy import units as u >>> thermal_speed(5*u.eV, 'p') <Quantity 30949.69018286 m / s> >>> thermal_speed(1e6*u.K, particle='p') <Quantity 128486.55193256 m / s> >>> thermal_speed(5*u.eV) <Quantity 1326205.12123959 m / s> >>> thermal_speed(1e6*u.K) <Quantity 5505693.98842538 m / s> >>> thermal_speed(1e6*u.K, method="rms") <Quantity 6743070.47577549 m / s> >>> thermal_speed(1e6*u.K, method="mean_magnitude") <Quantity 6212510.3969422 m / s> """ T = T.to(u.K, equivalencies=u.temperature_energy()) try: m = atomic.ion_mass(particle) except AtomicError: raise ValueError("Unable to find {particle} mass in thermal_speed") # different methods, as per https://en.wikipedia.org/wiki/Thermal_velocity if method == "most_probable": V = (np.sqrt(2 * k_B * T / m)).to(u.m / u.s) elif method == "rms": V = (np.sqrt(3 * k_B * T / m)).to(u.m / u.s) elif method == "mean_magnitude": V = (np.sqrt(8 * k_B * T / (m * np.pi))).to(u.m / u.s) else: raise ValueError("Method {method} not supported in thermal_speed") return V
def _check_quantity(arg, argname, funcname, units, can_be_negative=True, can_be_complex=False, can_be_inf=True): """ Raise an exception if an object is not a `~astropy.units.Quantity` withmcorrect units and valid numerical values. Parameters ---------- arg : ~astropy.units.Quantity The object to be tested. argname : str The name of the argument to be printed in error messages. funcname : str The name of the original function to be printed in error messages. units : ~astropy.units.Unit or list with ~astropy.units.Unit items Acceptable units for `arg`. can_be_negative : bool, optional `True` if the `~astropy.units.Quantity` can be negative, `False` otherwise. Defaults to `True`. can_be_complex : bool, optional `True` if the `~astropy.units.Quantity` can be a complex number, `False` otherwise. Defaults to `False`. can_be_inf : bool, optional `True` if the `~astropy.units.Quantity` can contain infinite values, `False` otherwise. Defaults to `True`. Raises ------ TypeError If the argument is not a `~astropy.units.Quantity` or units is not entirely units. ~astropy.units.UnitConversionError If the argument is not in acceptable units. ValueError If the argument contains any `~numpy.nan` or other invalid values as determined by the keywords. UserWarning If a `~astropy.units.Quantity` is not provided and unique units are provided, a `UserWarning` will be raised and the inputted units will be assumed. Examples -------- >>> from astropy import units as u >>> _check_quantity(4*u.T, 'B', 'f', u.T) """ # TODO: Replace `funcname` with func.__name__? if not isinstance(units, list): units = [units] for unit in units: if not isinstance(unit, (u.Unit, u.CompositeUnit, u.IrreducibleUnit)): raise TypeError("The keyword 'units' to check_quantity must be " "a unit or a list/tuple containing only units.") # Create a generic error message typeerror_message = ("The argument " + argname + " to " + funcname + " should be a Quantity with ") if len(units) == 1: typeerror_message += "the following units: " + str(units[0]) else: typeerror_message += "one of the following units: " for unit in units: typeerror_message += str(unit) if unit != units[-1]: typeerror_message += ", " if isinstance(arg, (u.Unit, u.CompositeUnit, u.IrreducibleUnit)): raise TypeError(typeerror_message) # Make sure arg is a quantity with correct units if not isinstance(arg, (u.Quantity)): if len(units) != 1: raise TypeError(typeerror_message) else: try: arg = arg*units[0] except Exception: raise TypeError(typeerror_message) else: raise UserWarning("No units are specified for " + argname + " in " + funcname + ". Assuming units of " + str(units[0]) + ".") in_acceptable_units = [] for unit in units: try: arg.unit.to(unit, equivalencies=u.temperature_energy()) except Exception: in_acceptable_units.append(False) else: in_acceptable_units.append(True) if not np.any(in_acceptable_units): raise u.UnitConversionError(typeerror_message) # Make sure that the quantity has valid numerical values valueerror_message = ("The argument " + argname + " to function " + funcname + " cannot contain ") if np.any(np.isnan(arg.value)): raise ValueError(valueerror_message + "NaNs.") elif np.any(np.iscomplex(arg.value)) and not can_be_complex: raise ValueError(valueerror_message + "complex numbers.") elif not can_be_negative and np.any(arg.value < 0): raise ValueError(valueerror_message + "negative numbers.") elif not can_be_inf and np.any(np.isinf(arg.value)): raise ValueError(valueerror_message + "infs.")
def Maxwellian_1D(v, T, particle="e", V_drift=0, vTh=np.nan, units="units"): r""" Returns the probability at the velocity `v` in m/s to find a particle `particle` in a plasma of temperature `T` following the Maxwellian distribution function. Parameters ---------- v: ~astropy.units.Quantity The velocity in units convertible to m/s. T: ~astropy.units.Quantity The temperature in Kelvin. particle: str, optional Representation of the particle species(e.g., `'p'` for protons, `'D+'` for deuterium, or `'He-4 +1'` for :math:`He_4^{+1}` (singly ionized helium-4), which defaults to electrons. V_drift: ~astropy.units.Quantity, optional The drift velocity in units convertible to m/s. vTh: ~astropy.units.Quantity, optional Thermal velocity (most probable) in m/s. This is used for optimization purposes to avoid re-calculating vTh, for example when integrating over velocity-space. units: str, optional Selects whether to run function with units and unit checks (when equal to "units") or to run as unitless (when equal to "unitless"). The unitless version is substantially faster for intensive computations. Returns ------- f : ~astropy.units.Quantity Probability in Velocity^-1, normalized so that :math:`\int_{-\infty}^{+\infty} f(v) dv = 1`. Raises ------ TypeError The parameter arguments are not Quantities and cannot be converted into Quantities. ~astropy.units.UnitConversionError If the parameters are not in appropriate units. ValueError If the temperature is negative, or the particle mass or charge state cannot be found. Notes ----- In one dimension, the Maxwellian distribution function for a particle of mass m, velocity v, a drift velocity V and with temperature T is: .. math:: f = \sqrt{\frac{m}{2 \pi k_B T}} e^{-\frac{m}{2 k_B T} (v-V)^2} f = (\pi * v_Th^2)^{-1/2} e^{-(v - v_{drift})^2 / v_Th^2} where :math:`v_Th = \sqrt(2 k_B T / m)` is the thermal speed Examples -------- >>> from plasmapy.physics import Maxwellian_1D >>> from astropy import units as u >>> v=1*u.m/u.s >>> Maxwellian_1D(v=v, T= 30000*u.K, particle='e',V_drift=0*u.m/u.s) <Quantity 5.91632969e-07 s / m> """ if units == "units": # unit checks and conversions # checking velocity units v = v.to(u.m / u.s) # catching case where drift velocities have default values, they # need to be assigned units if V_drift == 0: if not isinstance(V_drift, astropy.units.quantity.Quantity): V_drift = V_drift * u.m / u.s # checking units of drift velocities V_drift = V_drift.to(u.m / u.s) # convert temperature to Kelvins T = T.to(u.K, equivalencies=u.temperature_energy()) if np.isnan(vTh): # get thermal velocity and thermal velocity squared vTh = (thermal_speed(T, particle=particle, method="most_probable")) elif not np.isnan(vTh): # check units of thermal velocity vTh = vTh.to(u.m / u.s) elif np.isnan(vTh) and units == "unitless": # assuming unitless temperature is in Kelvins vTh = (thermal_speed(T * u.K, particle=particle, method="most_probable")).si.value # Get thermal velocity squared vThSq = vTh ** 2 # Get square of relative particle velocity vSq = (v - V_drift) ** 2 # calculating distribution function coeff = (vThSq * np.pi) ** (-1 / 2) expTerm = np.exp(-vSq / vThSq) distFunc = coeff * expTerm if units == "units": return distFunc.to(u.s / u.m) elif units == "unitless": return distFunc
def Coulomb_logarithm(T, n_e, particles, V=None): r"""Estimates the Coulomb logarithm. Parameters ---------- T : Quantity Temperature in units of temperature or energy per particle, which is assumed to be equal for both the test particle and the target particle n_e : Quantity The electron density in units convertible to per cubic meter. particles : tuple A tuple containing string representations of the test particle (listed first) and the target particle (listed second) V : Quantity, optional The relative velocity between particles. If not provided, thermal velocity is assumed: :math:`\mu V^2 \sim 3 k_B T` where `mu` is the reduced mass. Returns ------- lnLambda : float or numpy.ndarray An estimate of the Coulomb logarithm that is accurate to roughly its reciprocal. Raises ------ ValueError If the mass or charge of either particle cannot be found, or any of the inputs contain incorrect values. UnitConversionError If the units on any of the inputs are incorrect UserWarning If the inputted velocity is greater than 80% of the speed of light. TypeError If the n_e, T, or V are not Quantities. Notes ----- The Coulomb logarithm is given by .. math:: \ln{\Lambda} \equiv \ln\left( \frac{b_{max}}{b_{min}} \right) where :math:`b_{min}` and :math:`b_{max}` are the inner and outer impact parameters for Coulomb collisions _[1]. The outer impact parameter is given by the Debye length: :math:`b_{min} = \lambda_D` which is a function of electron temperature and electron density. At distances greater than the Debye length, electric fields from other particles will be screened out due to electrons rearranging themselves. The choice of inner impact parameter is more controversial. There are two main possibilities. The first possibility is that the inner impact parameter corresponds to a deflection angle of 90 degrees. The second possibility is that the inner impact parameter is a de Broglie wavelength, :math:`\lambda_B` corresponding to the reduced mass of the two particles and the relative velocity between collisions. This function uses the standard practice of choosing the inner impact parameter to be the maximum of these two possibilities. Some inconsistencies exist in the literature on how to define the inner impact parameter _[2]. Errors associated with the Coulomb logarithm are of order its inverse If the Coulomb logarithm is of order unity, then the assumptions made in the standard analysis of Coulomb collisions are invalid. Examples -------- >>> from astropy import units as u >>> n = 1e19*units.m**-3 >>> T = 1e6*units.K >>> particles = ('e', 'p') >>> Coulomb_logarithm(T, n, particles) 14.748259780491056 >>> Coulomb_logarithm(T, n, particles, 1e6*u.m/u.s) 11.363478214139432 References ---------- .. [1] Physics of Fully Ionized Gases, L. Spitzer (1962) .. [2] Comparison of Coulomb Collision Rates in the Plasma Physics and Magnetically Confined Fusion Literature, W. Fundamenski and O.E. Garcia, EFDA–JET–R(07)01 (http://www.euro-fusionscipub.org/wp-content/uploads/2014/11/EFDR07001.pdf) """ if not isinstance(particles, (list, tuple)) or len(particles) != 2: raise ValueError("The third input of Coulomb_logarithm must be a list " "or tuple containing representations of two charged " "particles.") masses = np.zeros(2) * units.kg charges = np.zeros(2) * units.C for particle, i in zip(particles, range(2)): try: masses[i] = ion_mass(particles[i]) except Exception: raise ValueError("Unable to find mass of particle: " + str(particles[i]) + " in Coulomb_logarithm.") try: charges[i] = np.abs(e * charge_state(particles[i])) if charges[i] is None: raise except Exception: raise ValueError("Unable to find charge of particle: " + str(particles[i]) + " in Coulomb_logarithm.") reduced_mass = masses[0] * masses[1] / (masses[0] + masses[1]) # The outer impact parameter is the Debye length. At distances # greater than the Debye length, the electrostatic potential of a # single particle is screened out by the electrostatic potentials # of other particles. Past this distance, the electric fields of # individual particles do not affect each other much. This # expression neglects screening by heavier ions. T = T.to(units.K, equivalencies=units.temperature_energy()) b_max = Debye_length(T, n_e) # The choice of inner impact parameter is more controversial. # There are two broad possibilities, and the expressions in the # literature often differ by factors of order unity or by # interchanging the reduced mass with the test particle mass. # The relative velocity is a source of uncertainty. It is # reasonable to make an assumption relating the thermal energy to # the kinetic energy: reduced_mass*velocity**2 is approximately # equal to 3*k_B*T. # If no relative velocity is inputted, then we make an assumption # that relates the thermal energy to the kinetic energy: # reduced_mass*velocity**2 is approximately equal to 3*k_B*T. if V is None: V = np.sqrt(3 * k_B * T / reduced_mass) else: _check_relativistic(V, 'Coulomb_logarithm', betafrac=0.8) # The first possibility is that the inner impact parameter # corresponds to a deflection of 90 degrees, which is valid when # classical effects dominate. b_perp = charges[0] * charges[1] / (4 * pi * eps0 * reduced_mass * V**2) # The second possibility is that the inner impact parameter is a # de Broglie wavelength. There remains some ambiguity as to which # mass to choose to go into the de Broglie wavelength calculation. # Here we use the reduced mass, which will be of the same order as # mass of the smaller particle and thus the longer de Broglie # wavelength. b_deBroglie = hbar / (2 * reduced_mass * V) # Coulomb-style collisions will not happen for impact parameters # shorter than either of these two impact parameters, so we choose # the larger of these two possibilities. b_min = np.zeros_like(b_perp) for i in range(b_min.size): if b_perp.flat[i] > b_deBroglie.flat[i]: b_min.flat[i] = b_perp.flat[i] else: b_min.flat[i] = b_deBroglie.flat[i] # Now that we know how many approximations have to go into plasma # transport theory, we shall celebrate by returning the Coulomb # logarithm. ln_Lambda = np.log(b_max / b_min) ln_Lambda = ln_Lambda.to(units.dimensionless_unscaled).value return ln_Lambda
def Maxwellian_speed_3D(vx, vy, vz, T, particle="e", Vx_drift=0, Vy_drift=0, Vz_drift=0, vTh=np.nan, units="units"): r""" Return the probability of finding a particle with speed components `vx`, `vy`, and `vz`in m/s in an equilibrium plasma of temperature `T` which follows the 3D Maxwellian distribution function. This function assumes Cartesian coordinates. Parameters ---------- vx: ~astropy.units.Quantity The speed in x-direction units convertible to m/s. vy: ~astropy.units.Quantity The speed in y-direction units convertible to m/s. vz: ~astropy.units.Quantity The speed in z-direction units convertible to m/s. T: ~astropy.units.Quantity The temperature, preferably in Kelvin. particle: str, optional Representation of the particle species(e.g., 'p' for protons, 'D+' for deuterium, or 'He-4 +1' for :math:`He_4^{+1}` (singly ionized helium-4), which defaults to electrons. Vx_drift: ~astropy.units.Quantity The drift speed in x-direction units convertible to m/s. Vy_drift: ~astropy.units.Quantity The drift speed in y-direction units convertible to m/s. Vz_drift: ~astropy.units.Quantity The drift speed in z-direction units convertible to m/s. vTh: ~astropy.units.Quantity, optional Thermal velocity (most probable) in m/s. This is used for optimization purposes to avoid re-calculating vTh, for example when integrating over velocity-space. units: str, optional Selects whether to run function with units and unit checks (when equal to "units") or to run as unitless (when equal to "unitless"). The unitless version is substantially faster for intensive computations. Returns ------- f : ~astropy.units.Quantity Probability in speed^-1, normalized so that: :math:`\iiint_{0}^{\infty} f(\vec{v}) d\vec{v} = 1`. Raises ------ TypeError A parameter argument is not a `~astropy.units.Quantity` and cannot be converted into a `~astropy.units.Quantity`. ~astropy.units.UnitConversionError If the parameters is not in appropriate units. ValueError If the temperature is negative, or the particle mass or charge state cannot be found. Notes ----- In one dimension, the Maxwellian speed distribution function describing the distribution of particles with speed :math:`v` in a plasma with temperature :math:`T` is given by: .. math:: f = 4 \pi \vec{v}^2 (\pi * v_Th^2)^{-3/2} \exp(-(\vec{v} - \vec{V_{drift}})^2 / v_Th^2) where :math:`v_Th = \sqrt(2 k_B T / m)` is the thermal speed. See also -------- Maxwellian_speed_1D Example ------- >>> from plasmapy.physics import Maxwellian_speed_3D >>> from astropy import units as u >>> v=1*u.m/u.s >>> Maxwellian_speed_3D(vx=v, ... vy=v, ... vz=v, ... T=30000*u.K, ... particle='e', ... Vx_drift=0*u.m/u.s, ... Vy_drift=0*u.m/u.s, ... Vz_drift=0*u.m/u.s) <Quantity 1.76238544e-53 s3 / m3> """ if units == "units": # unit checks and conversions # checking velocity units vx = vx.to(u.m / u.s) vy = vy.to(u.m / u.s) vz = vz.to(u.m / u.s) # catching case where drift velocities have default values, they # need to be assigned units if Vx_drift == 0: if not isinstance(Vx_drift, astropy.units.quantity.Quantity): Vx_drift = Vx_drift * u.m / u.s if Vy_drift == 0: if not isinstance(Vy_drift, astropy.units.quantity.Quantity): Vy_drift = Vy_drift * u.m / u.s if Vz_drift == 0: if not isinstance(Vz_drift, astropy.units.quantity.Quantity): Vz_drift = Vz_drift * u.m / u.s Vx_drift = Vx_drift.to(u.m / u.s) Vy_drift = Vy_drift.to(u.m / u.s) Vz_drift = Vz_drift.to(u.m / u.s) # convert temperature to Kelvins T = T.to(u.K, equivalencies=u.temperature_energy()) if np.isnan(vTh): # get thermal velocity and thermal velocity squared vTh = (thermal_speed(T, particle=particle, method="most_probable")) elif not np.isnan(vTh): # check units of thermal velocity vTh = vTh.to(u.m / u.s) elif np.isnan(vTh) and units == "unitless": # assuming unitless temperature is in Kelvins vTh = (thermal_speed(T * u.K, particle=particle, method="most_probable")).si.value # getting distribution functions along each axis fx = Maxwellian_speed_1D(vx, T, particle=particle, V_drift=Vx_drift, vTh=vTh, units=units) fy = Maxwellian_speed_1D(vy, T, particle=particle, V_drift=Vy_drift, vTh=vTh, units=units) fz = Maxwellian_speed_1D(vz, T, particle=particle, V_drift=Vz_drift, vTh=vTh, units=units) # multiplying probabilities in each axis to get 3D probability distFunc = fx * fy * fz if units == "units": return distFunc.to((u.s / u.m)**3) elif units == "unitless": return distFunc
def cutout_id_chem_map(yslice=slice(367,467), xslice=slice(114,214), vrange=[51,60]*u.km/u.s, sourcename='e2', filelist=glob.glob(paths.dpath('12m/cutouts/*e2e8*fits')), source=None, radius=None, chem_name='CH3OH', shape=None, # check that shape matches slice ): assert filelist maps = {} map_error = {} energies = {} degeneracies = {} frequencies = {} indices = {} # sanity check #shapes = [fits.getdata(fn).shape for fn in filelist] #assert len(set(shapes)) == 1 for ii,fn in enumerate(ProgressBar(filelist)): if chem_name not in fn: log.debug("Skipping {0} because it doesn't have {1}".format(fn, chem_name)) continue if 'temperature' in fn or 'column' in fn: continue if 'moment0' in fn: # there is a slight danger of off-by-one-pixel errors with the # cropping used here. Ideally, we'd reproject... m0_full = fits.getdata(fn) #stddev = fits.getdata(fn.replace("moment0","madstd")) header = fits.getheader(fn) cutout = Cutout2D(m0_full, source, 2*radius, wcs=wcs.WCS(header)) m0 = cutout.data #cutout_std = Cutout2D(stddev, source, 2*radius, wcs=wcs.WCS(header)) #stddev = cutout_std.data try: beam = radio_beam.Beam.from_fits_header(header) jtok = beam.jtok(header['RESTFRQ']*u.Hz).value except: jtok = 222. # approximated 0.32x0.34 at 225 GHz m0 = m0 * jtok # stddev = error in a single channel... not very accurate #stddev = stddev * jtok print("stddev for {0}".format(fn)) # this debug statement prevents abort traps stddev = mad_std(m0_full[np.isfinite(m0_full)]) * jtok header = cutout.wcs.to_header() else: cube_ = SpectralCube.read(fn) if shape: # have to have same shapes, otherwise pixel slices don't make sense assert cube_.shape == shape cube = cube_[:,yslice,xslice] bm = cube.beams[0] #jtok = bm.jtok(cube.wcs.wcs.restfrq*u.Hz) cube = cube.to(u.K, bm.jtok_equiv(cube.wcs.wcs.restfrq*u.Hz)) slab = cube.spectral_slab(*vrange) cube.beam_threshold = 1 #contguess = cube.spectral_slab(0*u.km/u.s, 40*u.km/u.s).percentile(50, axis=0) #contguess = cube.spectral_slab(70*u.km/u.s, 100*u.km/u.s).percentile(50, axis=0) mask = (cube.spectral_axis<40*u.km/u.s) | (cube.spectral_axis > 75*u.km/u.s) contguess = cube.with_mask(mask[:,None,None]).percentile(30, axis=0) stddev = cube.with_mask(mask[:,None,None]).apply_numpy_function(mad_std, axis=0) slabsub = (slab-contguess) slabsub.beam_threshold = 0.15 m0 = slabsub.moment0() header = m0.hdu.header label = linere.search(fn).groups()[0] frq = name_to_freq[label] closest_ind = np.argmin(np.abs(frqs - frq)) closest_key = list(rt.keys())[closest_ind] closest_rt = rt[closest_key] upperstate = ch3oh.data['States'][closest_rt.UpperStateRef] upperen = u.Quantity(float(upperstate.StateEnergyValue), unit=upperstate.StateEnergyUnit) maps[label] = m0 map_error[label] = stddev energies[label] = upperen degeneracies[label] = int(upperstate.TotalStatisticalWeight) indices[label] = closest_ind frequencies[label] = frq # make sure the dict indices don't change order energy_to_key = {v:k for k,v in energies.items()} order = sorted(energy_to_key.keys()) keys = [energy_to_key[k] for k in order] cube = np.empty((len(maps),)+maps[label].shape) ecube = np.empty_like(cube) xaxis = u.Quantity([energies[k] for k in keys]) xaxis = xaxis.to(u.erg, u.spectral()).to(u.K, u.temperature_energy()) for ii,key in enumerate(keys): # divide by degeneracy cube[ii,:,:] = maps[key] ecube[ii,:,:] = map_error[key] frequencies = u.Quantity([frequencies[k] for k in keys]) indices = [indices[k] for k in keys] degeneracies = [degeneracies[k] for k in keys] assert xaxis.size == cube.shape[0] return xaxis,cube,ecube,maps,map_error,energies,frequencies,indices,degeneracies,header
def kappa_velocity_3D(vx, vy, vz, T, kappa, particle="e", Vx_drift=0, Vy_drift=0, Vz_drift=0, vTh=np.nan, units="units"): r""" Return the probability of finding a particle with velocity components `v_x`, `v_y`, and `v_z`in m/s in a suprathermal plasma of temperature `T` and parameter 'kappa' which follows the 3D Kappa distribution function. This function assumes Cartesian coordinates. Parameters ---------- vx: ~astropy.units.Quantity The velocity in x-direction units convertible to m/s. vy: ~astropy.units.Quantity The velocity in y-direction units convertible to m/s. vz: ~astropy.units.Quantity The velocity in z-direction units convertible to m/s. T: ~astropy.units.Quantity The temperature, preferably in Kelvin. kappa: ~astropy.units.Quantity The kappa parameter is a dimensionless number which sets the slope of the energy spectrum of suprathermal particles forming the tail of the Kappa velocity distribution function. Kappa must be greater than :math:`3/2`. particle: str, optional Representation of the particle species(e.g., 'p' for protons, 'D+' for deuterium, or 'He-4 +1' for :math:`He_4^{+1}` : singly ionized helium-4), which defaults to electrons. Vx_drift: ~astropy.units.Quantity, optional The drift velocity in x-direction units convertible to m/s. Vy_drift: ~astropy.units.Quantity, optional The drift velocity in y-direction units convertible to m/s. Vz_drift: ~astropy.units.Quantity, optional The drift velocity in z-direction units convertible to m/s. vTh: ~astropy.units.Quantity, optional Thermal velocity (most probable) in m/s. This is used for optimization purposes to avoid re-calculating `vTh`, for example when integrating over velocity-space. units: str, optional Selects whether to run function with units and unit checks (when equal to "units") or to run as unitless (when equal to "unitless"). The unitless version is substantially faster for intensive computations. Returns ------- f : ~astropy.units.Quantity probability in Velocity^-1, normalized so that: :math:`\iiint_{0}^{\infty} f(\vec{v}) d\vec{v} = 1` Raises ------ TypeError The parameter arguments are not Quantities and cannot be converted into Quantities. ~astropy.units.UnitConversionError If the parameters is not in appropriate units. ValueError If the temperature is negative, or the particle mass or charge state cannot be found. Notes ----- In three dimensions, the Kappa velocity distribution function describing the distribution of particles with speed :math:`v` in a plasma with temperature :math:`T` and suprathermal parameter :math:`\kappa` is given by: .. math:: f = A_\kappa \left(1 + \frac{(\vec{v} - \vec{V_{drift}})^2}{\kappa v_Th,\kappa^2}\right)^{-(\kappa + 1)} where :math:`v_Th,\kappa` is the kappa thermal speed and :math:`A_\kappa = \frac{1}{2 \pi (\kappa v_Th,\kappa^2)^{3/2}} \frac{\Gamma(\kappa + 1)}{\Gamma(\kappa - 1/2) \Gamma(3/2)}` is the normalization constant. As :math:`\kappa` approaches infinity, the kappa distribution function converges to the Maxwellian distribution function. See also -------- kappa_velocity_1D kappa_thermal_speed Example ------- >>> from plasmapy.physics import kappa_velocity_3D >>> from astropy import units as u >>> v=1*u.m/u.s >>> kappa_velocity_3D(vx=v, ... vy=v, ... vz=v, ... T=30000*u.K, ... kappa=4, ... particle='e', ... Vx_drift=0*u.m/u.s, ... Vy_drift=0*u.m/u.s, ... Vz_drift=0*u.m/u.s) <Quantity 3.7833988e-19 s3 / m3> """ # must have kappa > 3/2 for distribution function to be valid if kappa <= 3 / 2: raise ValueError(f"Must have kappa > 3/2, instead of {kappa}.") if units == "units": # unit checks and conversions # checking velocity units vx = vx.to(u.m / u.s) vy = vy.to(u.m / u.s) vz = vz.to(u.m / u.s) # catching case where drift velocities have default values, they # need to be assigned units if Vx_drift == 0: if not isinstance(Vx_drift, astropy.units.quantity.Quantity): Vx_drift = Vx_drift * u.m / u.s if Vy_drift == 0: if not isinstance(Vy_drift, astropy.units.quantity.Quantity): Vy_drift = Vy_drift * u.m / u.s if Vz_drift == 0: if not isinstance(Vz_drift, astropy.units.quantity.Quantity): Vz_drift = Vz_drift * u.m / u.s # checking units of drift velocities Vx_drift = Vx_drift.to(u.m / u.s) Vy_drift = Vy_drift.to(u.m / u.s) Vz_drift = Vz_drift.to(u.m / u.s) # convert temperature to Kelvins T = T.to(u.K, equivalencies=u.temperature_energy()) if np.isnan(vTh): # get thermal velocity and thermal velocity squared vTh = kappa_thermal_speed(T, kappa, particle=particle) elif not np.isnan(vTh): # check units of thermal velocity vTh = vTh.to(u.m / u.s) elif np.isnan(vTh) and units == "unitless": # assuming unitless temperature is in Kelvins vTh = (kappa_thermal_speed(T * u.K, kappa, particle=particle)).si.value # getting square of thermal velocity vThSq = vTh**2 # Get square of relative particle velocity vSq = ((vx - Vx_drift)**2 + (vy - Vy_drift)**2 + (vz - Vz_drift)**2) # calculating distribution function expTerm = (1 + vSq / (kappa * vThSq))**(-(kappa + 1)) coeff1 = 1 / (2 * np.pi * (kappa * vThSq)**(3 / 2)) coeff2 = gamma(kappa + 1) / (gamma(kappa - 1 / 2) * gamma(3 / 2)) distFunc = coeff1 * coeff2 * expTerm if units == "units": return distFunc.to((u.s / u.m)**3) elif units == "unitless": return distFunc
For example, plasmas at high (much larger than 1) Reynolds numbers are highly turbulent, while turbulence is negligible at low Reynolds numbers. """ __all__ = ['beta', 'quantum_theta'] from astropy import constants from astropy import units as u from plasmapy.formulary import (quantum, parameters) from plasmapy.utils.decorators import validate_quantities @validate_quantities(T={ 'can_be_negative': False, 'equivalencies': u.temperature_energy() }, n_e={'can_be_negative': False}) def quantum_theta(T: u.K, n_e: u.m**-3) -> u.dimensionless_unscaled: """ Compares Fermi energy to thermal kinetic energy to check if quantum effects are important. Parameters ---------- T : ~astropy.units.Quantity The temperature of the plasma. n_e : ~astropy.units.Quantity The electron number density of the plasma. Examples
import numpy as np import warnings from astropy.constants.si import c, e, eps0, k_B from plasmapy import particles from plasmapy.formulary import frequencies, speeds from plasmapy.particles import Particle from plasmapy.utils.decorators import validate_quantities from plasmapy.utils.exceptions import PlasmaPyFutureWarning __all__ += __aliases__ @validate_quantities( T_e={"can_be_negative": False, "equivalencies": u.temperature_energy()}, n_e={"can_be_negative": False}, ) def Debye_length(T_e: u.K, n_e: u.m**-3) -> u.m: r"""Calculate the characteristic decay length for electric fields, due to charge screening. **Aliases:** `lambdaD_` Parameters ---------- T_e : `~astropy.units.Quantity` Electron temperature. n_e : `~astropy.units.Quantity` Electron number density.
def cutout_id_chem_map( yslice=slice(367, 467), xslice=slice(114, 214), vrange=[51, 60] * u.km / u.s, sourcename='e2', filelist=glob.glob(paths.dpath('merge/cutouts/*natural*e2e8*fits')), linere=re.compile("W51_b6_7M_12M_natural.(.*).image.pbcor"), chem_name='HNCO', ): maps = {} energies = {} degeneracies = {} frequencies = {} indices = {} assert len(filelist) > 1 for ii, fn in enumerate(ProgressBar(filelist)): if chem_name not in fn: log.debug("Skipping {0} because it doesn't have {1}".format( fn, chem_name)) continue if 'temperature' in fn or 'column' in fn: continue cube = SpectralCube.read(fn)[:, yslice, xslice] print(yslice, xslice) print(cube) bm = cube.beams[0] #jtok = bm.jtok(cube.wcs.wcs.restfrq*u.Hz) cube = cube.to(u.K, bm.jtok_equiv(cube.wcs.wcs.restfrq * u.Hz)) slab = cube.spectral_slab(*vrange) cube.beam_threshold = 1 #contguess = cube.spectral_slab(0*u.km/u.s, 40*u.km/u.s).percentile(50, axis=0) #contguess = cube.spectral_slab(70*u.km/u.s, 100*u.km/u.s).percentile(50, axis=0) mask = (cube.spectral_axis < 40 * u.km / u.s) | (cube.spectral_axis > 75 * u.km / u.s) contguess = cube.with_mask(mask[:, None, None]).percentile(30, axis=0) slabsub = (slab - contguess) slabsub.beam_threshold = 0.15 m0 = slabsub.moment0() label = linere.search(fn).groups()[0] frq = name_to_freq[label] closest_ind = np.argmin(np.abs(frqs - frq)) closest_key = list(rt.keys())[closest_ind] closest_rt = rt[closest_key] upperstate = hnco.data['States'][closest_rt.UpperStateRef] upperen = u.Quantity(float(upperstate.StateEnergyValue), unit=upperstate.StateEnergyUnit) maps[label] = m0 energies[label] = upperen degeneracies[label] = int(upperstate.TotalStatisticalWeight) indices[label] = closest_ind frequencies[label] = frq # make sure the dict indices don't change order energy_to_key = {v: k for k, v in energies.items()} order = sorted(energy_to_key.keys()) keys = [energy_to_key[k] for k in order] cube = np.empty((len(maps), ) + maps[label].shape) xaxis = u.Quantity([energies[k] for k in keys]) xaxis = xaxis.to(u.erg, u.spectral()).to(u.K, u.temperature_energy()) for ii, key in enumerate(keys): # divide by degeneracy cube[ii, :, :] = maps[key] frequencies = u.Quantity([frequencies[k] for k in keys]) indices = [indices[k] for k in keys] degeneracies = [degeneracies[k] for k in keys] return xaxis, cube, maps, energies, frequencies, indices, degeneracies, m0.hdu.header
"""Functions related to ionization states and the properties thereof.""" __all__ = ["ionization_balance", "Saha", "Z_bal_"] __aliases__ = ["Z_bal_"] import astropy.units as u from astropy.constants import a0, k_B from numpy import exp, log, pi, sqrt from plasmapy.utils.decorators import validate_quantities @validate_quantities(T_e={ "can_be_negative": False, "equivalencies": u.temperature_energy() }) def ionization_balance(n: u.m**-3, T_e: u.K) -> u.dimensionless_unscaled: r""" Return the average ionization state of ions in a plasma assuming that the numbers of ions in each state are equal. Z_bal is the estimate average ionization level of a plasma in thermal equilibrium that balances the number density of ions in two different ionization states. Z_bal is derived from the Saha equation with the assumptions that the atoms are of a single species, are either hydrogenic or completely ionized, and that there is a balance between ionization and recombination, meaning that the number of atoms in either state are equal. The Saha equation and therefore Z_bal are more accurate when the plasma is at a high density and temperature.
def thermal_speed(T, particle="e", method="most_probable"): r"""Returns the most probable speed for an particle within a Maxwellian distribution. Parameters ---------- T : Quantity The particle temperature in either kelvin or energy per particle particle : string, optional Representation of the particle species (e.g., 'p' for protons, 'D+' for deuterium, or 'He-4 +1' for singly ionized helium-4), which defaults to electrons. If no charge state information is provided, then the particles are assumed to be singly charged. Returns ------- V : Quantity particle thermal speed Raises ------ TypeError The particle temperature is not a Quantity UnitConversionError If the particle temperature is not in units of temperature or energy per particle ValueError The particle temperature is invalid or particle cannot be used to identify an isotope or particle UserWarning If the particle thermal speed exceeds 10% of the speed of light, or if units are not provided and SI units are assumed. Notes ----- The particle thermal speed is given by: .. math:: V_{th,i} = \sqrt{\frac{2 k_B T_i}{m_i}} This function yields the most probable speed within a distribution function. However, the definition of thermal velocity varies by the square root of two depending on whether or not this velocity absorbs that factor in the expression for a Maxwellian distribution. In particular, the expression given in the NRL Plasma Formulary [1] is a square root of two smaller than the result from this function. Examples -------- >>> from astropy import units as u >>> thermal_speed(5*u.eV, 'p') <Quantity 30949.690182856546 m / s> >>> thermal_speed(1e6*u.K, particle='p') <Quantity 128486.55193256242 m / s> >>> thermal_speed(5*u.eV) <Quantity 1326205.1212395933 m / s> >>> thermal_speed(1e6*u.K) <Quantity 5505693.988425379 m / s> >>> thermal_speed(1e6*u.K, method="rms") <Quantity 6743070.475775486 m / s> >>> thermal_speed(1e6*u.K, method="mean_magnitude") <Quantity 19517177.023383822 m / s> """ T = T.to(units.K, equivalencies=units.temperature_energy()) try: m = ion_mass(particle) except Exception: raise ValueError( "Unable to find {} mass in thermal_speed".format(particle)) # different methods, as per https://en.wikipedia.org/wiki/Thermal_velocity if method == "most_probable": V = (np.sqrt(2 * k_B * T / m)).to(units.m / units.s) elif method == "rms": V = (np.sqrt(3 * k_B * T / m)).to(units.m / units.s) elif method == "mean_magnitude": V = (np.sqrt(8 * k_B * T / (m / np.pi))).to(units.m / units.s) else: raise (ValueError( "Method {} not supported in thermal_speed".format(method))) return V
def test_temperature_energy(): x = 1000 * u.K y = (x * constants.k_B).to(u.keV) assert_allclose(x.to_value(u.keV, u.temperature_energy()), y.value) assert_allclose(y.to_value(u.K, u.temperature_energy()), x.value)
def Debye_length(T_e, n_e): r"""Calculate the Debye length. Parameters ---------- T_e: Quantity Electron temperature n_e: Quantity Electron number density Returns ------- lambda_D: Quantity The Debye length in meters Raises ------ TypeError If either argument is not a Quantity UnitConversionError If either argument is in incorrect units ValueError If either argument contains invalid values UserWarning If units are not provided and SI units are assumed Notes ----- The Debye length is the exponential scale length for charge screening and is given by .. math:: \lambda_D = \sqrt{\frac{\epsilon_0 k_b T_e}{n_e e^2}} for an electron plasma with nearly stationary ions. The electrical potential will drop by a factor of 1/e every Debye length. Plasmas will generally be quasineutral on length scales significantly larger than the Debye length. See also -------- Debye_number Example ------- >>> from astropy import units as u >>> Debye_length(5e6*u.K, 5e15*u.m**-3) <Quantity 0.002182255218625608 m> """ T_e = T_e.to(units.K, equivalencies=units.temperature_energy()) try: lambda_D = ((eps0 * k_B * T_e / (n_e * e**2))**0.5).to(units.m) except Exception: raise ValueError("Unable to find Debye length.") return lambda_D
def _check_quantity(arg, argname, funcname, units, can_be_negative=True, can_be_complex=False, can_be_inf=True, can_be_nan=True, none_shall_pass=False): """ Raise an exception if an object is not a `~astropy.units.Quantity` with correct units and valid numerical values. Parameters ---------- arg : ~astropy.units.Quantity The object to be tested. argname : str The name of the argument to be printed in error messages. funcname : str The name of the original function to be printed in error messages. units : `~astropy.units.Unit` or list of `~astropy.unit.Unit` Acceptable units for `arg`. can_be_negative : bool, optional `True` if the `~astropy.units.Quantity` can be negative, `False` otherwise. Defaults to `True`. can_be_complex : bool, optional `True` if the `~astropy.units.Quantity` can be a complex number, `False` otherwise. Defaults to `False`. can_be_inf : bool, optional `True` if the `~astropy.units.Quantity` can contain infinite values, `False` otherwise. Defaults to `True`. can_be_nan : bool, optional `True` if the `~astropy.units.Quantity` can contain NaN values, `False` otherwise. Defaults to `True`. none_shall_pass : bool, optional `True` if the `~astropy.units.Quantity` can contain None values, `False` otherwise. Defaults to `True`. Raises ------ TypeError If the argument is not a `~astropy.units.Quantity` or units is not entirely units. ~astropy.units.UnitConversionError If the argument is not in acceptable units. ~astropy.units.UnitsError If after the assumption checks, the argument is still not in acceptable units. ValueError If the argument contains any `~numpy.nan` or other invalid values as determined by the keywords. Warns ----- ~astropy.units.UnitsWarning If a `~astropy.units.Quantity` is not provided and unique units are provided, a `UnitsWarning` will be raised and the inputted units will be assumed. Examples -------- >>> from astropy import units as u >>> import pytest >>> _check_quantity(4*u.T, 'B', 'f', u.T) <Quantity 4. T> >>> with pytest.warns(u.UnitsWarning, match="No units are specified"): ... assert _check_quantity(4, 'B', 'f', u.T) == 4 * u.T """ # TODO: Replace `funcname` with func.__name__? if not isinstance(units, list): units = [units] for unit in units: if not isinstance(unit, (u.Unit, u.CompositeUnit, u.IrreducibleUnit)): raise TypeError("The keyword 'units' to check_quantity must be " "a unit or a list/tuple containing only units.") # Create a generic error message typeerror_message = ( f"The argument {argname} to {funcname} should be a Quantity with ") if len(units) == 1: typeerror_message += f"the following units: {str(units[0])}" else: typeerror_message += "one of the following units: " for unit in units: typeerror_message += str(unit) if unit != units[-1]: typeerror_message += ", " if none_shall_pass: typeerror_message += "or None " if isinstance(arg, (u.Unit, u.CompositeUnit, u.IrreducibleUnit)): raise TypeError(typeerror_message) # Make sure arg is a quantity with correct units unit_casting_warning = dedent( f"""No units are specified for {argname} = {arg} in {funcname}. Assuming units of {str(units[0])}. To silence this warning, explicitly pass in an Astropy Quantity (from astropy.units) (see http://docs.astropy.org/en/stable/units/)""") # TODO include explicit note on how to pass in Astropy Quantity valueerror_message = ( f"The argument {argname} to function {funcname} cannot contain") if arg is None and none_shall_pass: return arg elif arg is None: raise ValueError(f"{valueerror_message} Nones.") if not isinstance(arg, (u.Quantity)): if len(units) != 1: raise TypeError(typeerror_message) else: try: arg = arg * units[0] except (u.UnitsError, ValueError): raise TypeError(typeerror_message) else: warnings.warn(UnitsWarning(unit_casting_warning)) if not isinstance(arg, u.Quantity): raise u.UnitsError( "{} is still not a Quantity after checks!".format(arg)) in_acceptable_units = [] for unit in units: try: arg.unit.to(unit, equivalencies=u.temperature_energy()) except Exception: in_acceptable_units.append(False) else: in_acceptable_units.append(True) if not np.any(in_acceptable_units): raise u.UnitConversionError(typeerror_message) # Make sure that the quantity has valid numerical values if np.any(np.isnan(arg.value)) and not can_be_nan: raise ValueError(f"{valueerror_message} NaNs.") elif np.any(np.iscomplex(arg.value)) and not can_be_complex: raise ValueError(f"{valueerror_message} complex numbers.") elif not can_be_negative: # Allow NaNs through without raising a warning with np.errstate(invalid='ignore'): isneg = np.any(arg.value < 0) if isneg: raise ValueError(f"{valueerror_message} negative numbers.") elif not can_be_inf and np.any(np.isinf(arg.value)): raise ValueError(f"{valueerror_message} infs.") return arg
def ion_sound_speed(T_e, T_i, gamma_e=1, gamma_i=3, ion='p+', z_mean=None): r""" Return the ion sound speed for an electron-ion plasma. Parameters ---------- T_e : ~astropy.units.Quantity Electron temperature in units of temperature or energy per particle. If this is not given, then the electron temperature is assumed to be zero. T_i : ~astropy.units.Quantity Ion temperature in units of temperature or energy per particle. If this is not given, then the ion temperature is assumed to be zero. gamma_e : float or int The adiabatic index for electrons, which defaults to 1. This value assumes that the electrons are able to equalize their temperature rapidly enough that the electrons are effectively isothermal. gamma_i : float or int The adiabatic index for ions, which defaults to 3. This value assumes that ion motion has only one degree of freedom, namely along magnetic field lines. ion : str, optional Representation of the ion species (e.g., `'p'` for protons, `'D+'` for deuterium, or 'He-4 +1' for singly ionized helium-4), which defaults to protons. If no charge state information is provided, then the ions are assumed to be singly charged. z_mean : ~astropy.units.Quantity, optional The average ionization (arithmetic mean) for a plasma where the a macroscopic description is valid. If this quantity is not given then the atomic charge state (integer) of the ion is used. This is effectively an average ion sound speed for the plasma where multiple charge states are present. Returns ------- V_S : ~astropy.units.Quantity The ion sound speed in units of meters per second. Raises ------ TypeError If any of the arguments are not entered as keyword arguments or are of an incorrect type. ValueError If the ion mass, adiabatic index, or temperature are invalid. ~plasmapy.utils.PhysicsError If an adiabatic index is less than one. ~astropy.units.UnitConversionError If the temperature is in incorrect units. Warns ----- RelativityWarning If the ion sound speed exceeds 5% of the speed of light. ~astropy.units.UnitsWarning If units are not provided, SI units are assumed. Notes ----- The ion sound speed :math:`V_S` is approximately given by .. math:: V_S = \sqrt{\frac{\gamma_e Z k_B T_e + \gamma_i k_B T_i}{m_i}} where :math:`\gamma_e` and :math:`\gamma_i` are the electron and ion adiabatic indices, :math:`k_B` is the Boltzmann constant, :math:`T_e` and :math:`T_i` are the electron and ion temperatures, :math:`Z` is the charge state of the ion, and :math:`m_i` is the ion mass. This function assumes that the product of the wavenumber and the Debye length is small. In this limit, the ion sound speed is not dispersive. In other words, it is frequency independent. When the electron temperature is much greater than the ion temperature, the ion sound velocity reduces to :math:`\sqrt{\gamma_e k_B T_e / m_i}`. Ion acoustic waves can therefore occur even when the ion temperature is zero. Example ------- >>> from astropy import units as u >>> ion_sound_speed(T_e=5e6*u.K, T_i=0*u.K, ion='p', gamma_e=1, gamma_i=3) <Quantity 203155.0764042 m / s> >>> ion_sound_speed(T_e=5e6*u.K, T_i=0*u.K) <Quantity 203155.0764042 m / s> >>> ion_sound_speed(T_e=500*u.eV, T_i=200*u.eV, ion='D+') <Quantity 229586.01860212 m / s> """ m_i = atomic.ion_mass(ion) Z = grab_charge(ion, z_mean) for gamma, particles in zip([gamma_e, gamma_i], ["electrons", "ions"]): if not isinstance(gamma, (float, int)): raise TypeError( f"The adiabatic index gamma for {particles} must be " "a float or int") if gamma < 1: raise utils.PhysicsError( f"The adiabatic index for {particles} must be between " "one and infinity") T_i = T_i.to(u.K, equivalencies=u.temperature_energy()) T_e = T_e.to(u.K, equivalencies=u.temperature_energy()) try: V_S_squared = (gamma_e * Z * k_B * T_e + gamma_i * k_B * T_i) / m_i V_S = np.sqrt(V_S_squared).to(u.m / u.s) except Exception: raise ValueError("Unable to find ion sound speed.") return V_S
def Maxwellian_speed_1D(v, T, particle="e", V_drift=0, vTh=np.nan, units="units"): r""" Return the probability of finding a particle with speed `v` in m/s in an equilibrium plasma of temperature `T` which follows the Maxwellian distribution function. Parameters ---------- v: ~astropy.units.Quantity The speed in units convertible to m/s. T: ~astropy.units.Quantity The temperature, preferably in Kelvin. particle: str, optional Representation of the particle species(e.g., `'p'` for protons, `'D+'` for deuterium, or `'He-4 +1'` for :math:`He_4^{+1}` (singly ionized helium-4), which defaults to electrons. V_drift: ~astropy.units.Quantity The drift speed in units convertible to m/s. vTh: ~astropy.units.Quantity, optional Thermal velocity (most probable) in m/s. This is used for optimization purposes to avoid re-calculating vTh, for example when integrating over velocity-space. units: str, optional Selects whether to run function with units and unit checks (when equal to "units") or to run as unitless (when equal to "unitless"). The unitless version is substantially faster for intensive computations. Returns ------- f : ~astropy.units.Quantity Probability in speed^-1, normalized so that :math:`\int_{0}^{\infty} f(v) dv = 1`. Raises ------ TypeError The parameter arguments are not Quantities and cannot be converted into Quantities. ~astropy.units.UnitConversionError If the parameters is not in appropriate units. ValueError If the temperature is negative, or the particle mass or charge state cannot be found. Notes ----- In one dimension, the Maxwellian speed distribution function describing the distribution of particles with speed v in a plasma with temperature T is given by: .. math:: f(v) = 4 \pi v^2 (\pi * v_Th^2)^{-3/2} \exp(-(v - V_{drift})^2 / v_Th^2) where :math:`v_Th = \sqrt(2 k_B T / m)` is the thermal speed. Example ------- >>> from plasmapy.physics import Maxwellian_speed_1D >>> from astropy import units as u >>> v=1*u.m/u.s >>> Maxwellian_speed_1D(v=v, T= 30000*u.K, particle='e',V_drift=0*u.m/u.s) <Quantity 2.60235754e-18 s / m> """ if units == "units": # unit checks and conversions # checking velocity units v = v.to(u.m / u.s) # catching case where drift velocity has default value, and # needs to be assigned units if V_drift == 0: if not isinstance(V_drift, astropy.units.quantity.Quantity): V_drift = V_drift * u.m / u.s # checking drift velocity units V_drift = V_drift.to(u.m / u.s) # convert temperature to Kelvins T = T.to(u.K, equivalencies=u.temperature_energy()) if np.isnan(vTh): # get thermal velocity and thermal velocity squared vTh = (thermal_speed(T, particle=particle, method="most_probable")) elif not np.isnan(vTh): # check units of thermal velocity vTh = vTh.to(u.m / u.s) elif np.isnan(vTh) and units == "unitless": # assuming unitless temperature is in Kelvins vTh = (thermal_speed(T * u.K, particle=particle, method="most_probable")).si.value # getting square of thermal speed vThSq = vTh ** 2 # get square of relative particle speed vSq = (v - V_drift) ** 2 # calculating distribution function coeff1 = (np.pi * vThSq) ** (-3 / 2) coeff2 = 4 * np.pi * vSq expTerm = np.exp(-vSq / vThSq) distFunc = coeff1 * coeff2 * expTerm if units == "units": return distFunc.to(u.s / u.m) elif units == "unitless": return distFunc
def kappa_thermal_speed(T, kappa, particle="e-", method="most_probable"): r"""Return the most probable speed for a particle within a Kappa distribution. Parameters ---------- T : ~astropy.units.Quantity The particle temperature in either kelvin or energy per particle kappa: float The kappa parameter is a dimensionless number which sets the slope of the energy spectrum of suprathermal particles forming the tail of the Kappa velocity distribution function. Kappa must be greater than 3/2. particle : str, optional Representation of the particle species (e.g., 'p' for protons, 'D+' for deuterium, or 'He-4 +1' for singly ionized helium-4), which defaults to electrons. If no charge state information is provided, then the particles are assumed to be singly charged. method : str, optional Method to be used for calculating the thermal speed. Options are 'most_probable' (default), 'rms', and 'mean_magnitude'. Returns ------- V : ~astropy.units.Quantity Particle thermal speed Raises ------ TypeError The particle temperature is not a ~astropy.units.Quantity. astropy.units.UnitConversionError If the particle temperature is not in units of temperature or energy per particle. ValueError The particle temperature is invalid or particle cannot be used to identify an isotope or particle. Warns ----- RelativityWarning If the particle thermal speed exceeds 5% of the speed of light, or ~astropy.units.UnitsWarning If units are not provided, SI units are assumed. Notes ----- The particle thermal speed is given by: .. math:: V_{th,i} = \sqrt{(2 \kappa - 3)\frac{2 k_B T_i}{\kappa m_i}} For more discussion on the mean_magnitude calculation method, see [1]_. Examples -------- >>> from astropy import units as u >>> kappa_thermal_speed(5*u.eV, 4, 'p') # defaults to most probable <Quantity 24467.87846359 m / s> >>> kappa_thermal_speed(5*u.eV, 4, 'p', 'rms') <Quantity 37905.47432261 m / s> >>> kappa_thermal_speed(5*u.eV, 4, 'p', 'mean_magnitude') <Quantity 34922.9856304 m / s> References ---------- .. [1] PlasmaPy Issue #186, https://github.com/PlasmaPy/PlasmaPy/issues/186 See Also -------- plasmapy.physics.kappa_thermal_speed plasmapy.physics.kappa_velocity_1D """ # Checking thermal units T = T.to(u.K, equivalencies=u.temperature_energy()) if kappa <= 3 / 2: raise ValueError(f"Must have kappa > 3/2, instead of {kappa}, for " "kappa distribution function to be valid.") # different methods, as per https://en.wikipedia.org/wiki/Thermal_velocity vTh = thermal_speed(T=T, particle=particle, method=method) if method == "most_probable": # thermal velocity of Kappa distribution function is just Maxwellian # thermal speed modulated by the following factor. # This is only true for "most probable" case. RMS and mean # magnitude velocities are same as Maxwellian. coeff = np.sqrt((kappa - 3 / 2) / kappa) else: coeff = 1 return vTh * coeff
def cutout_id_chem_map( yslice=slice(367, 467), xslice=slice(114, 214), vrange=[51, 60] * u.km / u.s, sourcename="e2", filelist=glob.glob(paths.dpath("12m/cutouts/*e2e8*fits")), source=None, radius=None, molecular_database=ch3oh, radiative_transitions=rt, frqs=frqs, chem_name="CH3OH", shape=None, # check that shape matches slice ): assert filelist maps = {} map_error = {} energies = {} degeneracies = {} frequencies = {} indices = {} # sanity check # shapes = [fits.getdata(fn).shape for fn in filelist] # assert len(set(shapes)) == 1 for ii, fn in enumerate(ProgressBar(filelist)): if chem_name not in fn: log.debug("Skipping {0} because it doesn't have {1}".format(fn, chem_name)) continue if "temperature" in fn or "column" in fn: continue if "moment0" in fn: # there is a slight danger of off-by-one-pixel errors with the # cropping used here. Ideally, we'd reproject... m0_full = fits.getdata(fn) # stddev = fits.getdata(fn.replace("moment0","madstd")) header = fits.getheader(fn) cutout = Cutout2D(m0_full, source, 2 * radius, wcs=wcs.WCS(header)) m0 = cutout.data # cutout_std = Cutout2D(stddev, source, 2*radius, wcs=wcs.WCS(header)) # stddev = cutout_std.data try: beam = radio_beam.Beam.from_fits_header(header) jtok = beam.jtok(header["RESTFRQ"] * u.Hz).value except: jtok = 222.0 # approximated 0.32x0.34 at 225 GHz m0 = m0 * jtok # stddev = error in a single channel... not very accurate # stddev = stddev * jtok print("stddev for {0}".format(fn)) # this debug statement prevents abort traps stddev = mad_std(m0_full[np.isfinite(m0_full)]) * jtok header = cutout.wcs.to_header() else: cube_ = SpectralCube.read(fn) if shape: # have to have same shapes, otherwise pixel slices don't make sense assert cube_.shape == shape cube = cube_[:, yslice, xslice] bm = cube.beams[0] # jtok = bm.jtok(cube.wcs.wcs.restfrq*u.Hz) cube = cube.to(u.K, bm.jtok_equiv(cube.wcs.wcs.restfrq * u.Hz)) slab = cube.spectral_slab(*vrange) cube.beam_threshold = 1 # contguess = cube.spectral_slab(0*u.km/u.s, 40*u.km/u.s).percentile(50, axis=0) # contguess = cube.spectral_slab(70*u.km/u.s, 100*u.km/u.s).percentile(50, axis=0) mask = (cube.spectral_axis < 40 * u.km / u.s) | (cube.spectral_axis > 75 * u.km / u.s) contguess = cube.with_mask(mask[:, None, None]).percentile(30, axis=0) stddev = cube.with_mask(mask[:, None, None]).apply_numpy_function(mad_std, axis=0) slabsub = slab - contguess slabsub.beam_threshold = 0.15 m0 = slabsub.moment0() header = m0.hdu.header label = linere.search(fn).groups()[0] frq = name_to_freq[label] closest_ind = np.argmin(np.abs(frqs - frq)) closest_key = list(radiative_transitions.keys())[closest_ind] closest_rt = radiative_transitions[closest_key] upperstate = molecular_database.data["States"][closest_rt.UpperStateRef] upperen = u.Quantity(float(upperstate.StateEnergyValue), unit=upperstate.StateEnergyUnit) maps[label] = m0 map_error[label] = stddev energies[label] = upperen degeneracies[label] = int(upperstate.TotalStatisticalWeight) indices[label] = closest_ind frequencies[label] = frq # make sure the dict indices don't change order energy_to_key = {v: k for k, v in energies.items()} order = sorted(energy_to_key.keys()) keys = [energy_to_key[k] for k in order] cube = np.empty((len(maps),) + maps[label].shape) ecube = np.empty_like(cube) xaxis = u.Quantity([energies[k] for k in keys]) xaxis = xaxis.to(u.erg, u.spectral()).to(u.K, u.temperature_energy()) for ii, key in enumerate(keys): # divide by degeneracy cube[ii, :, :] = maps[key] ecube[ii, :, :] = map_error[key] frequencies = u.Quantity([frequencies[k] for k in keys]) indices = [indices[k] for k in keys] degeneracies = [degeneracies[k] for k in keys] assert xaxis.size == cube.shape[0] return xaxis, cube, ecube, maps, map_error, energies, frequencies, indices, degeneracies, header
def collision_rate_ion_ion(T_i, n_i, ion_particle, coulomb_log=None, V=None, coulomb_log_method="classical"): r""" Momentum relaxation ion-ion collision rate From [3]_, equations (2.36) and (2.122) Considering a Maxwellian distribution of "test" ions colliding with a Maxwellian distribution of "field" ions. Note, it is assumed that electrons are present in such numbers as to establish quasineutrality, but the effects of the test ions colliding with them are not considered here. This result is an ion momentum relaxation rate, and is used in many classical transport expressions. It is equivalent to: * 1/tau_i from ref [1]_ eqn (1) pp. #, * 1/tau_i from ref [2]_ eqn (1) pp. #, * nu_i\i_S from ref [2]_ eqn (1) pp. #, Parameters ---------- T_i : ~astropy.units.Quantity The electron temperature of the Maxwellian test ions n_i : ~astropy.units.Quantity The number density of the Maxwellian test ions ion_particle: str String signifying a particle type of the test and field ions, including charge state information. This function assumes the test and field ions are the same species. V : ~astropy.units.Quantity, optional The relative velocity between particles. If not provided, thermal velocity is assumed: :math:`\mu V^2 \sim 2 k_B T` where `mu` is the reduced mass. coulomb_log : float or dimensionless ~astropy.units.Quantity, optional Option to specify a Coulomb logarithm of the electrons on the ions. If not specified, the Coulomb log will is calculated using the ~plasmapy.physics.transport.Coulomb_logarithm function. coulomb_log_method : string, optional Method used for Coulomb logarithm calculation (see that function for more documentation). Choose from "classical" or "GMS-1" to "GMS-6". References ---------- .. [1] Braginskii .. [2] Formulary .. [3] Callen Chapter 2, http://homepages.cae.wisc.edu/~callen/chap2.pdf Examples -------- >>> from astropy import units as u >>> collision_rate_ion_ion(0.1*u.eV, 1e6/u.m**3, 'p') <Quantity 2.97315582e-05 1 / s> >>> collision_rate_ion_ion(100*u.eV, 1e6/u.m**3, 'p') <Quantity 1.43713193e-09 1 / s> >>> collision_rate_ion_ion(100*u.eV, 1e20/u.m**3, 'p') <Quantity 66411.80316364 1 / s> >>> collision_rate_ion_ion(100*u.eV, 1e20/u.m**3, 'p', coulomb_log_method='GMS-1') <Quantity 66407.00859126 1 / s> >>> collision_rate_ion_ion(100*u.eV, 1e20/u.m**3, 'p', V = c/100) <Quantity 6.53577473 1 / s> >>> collision_rate_ion_ion(100*u.eV, 1e20/u.m**3, 'p', coulomb_log=20) <Quantity 95918.76240877 1 / s> """ from plasmapy.physics.transport.collisions import Coulomb_logarithm T_i = T_i.to(u.K, equivalencies=u.temperature_energy()) m_i = atomic.ion_mass(ion_particle) if V is not None: V = V else: # ion thermal velocity (most probable) V = np.sqrt(2 * k_B * T_i / m_i) if coulomb_log is not None: coulomb_log_val = coulomb_log else: particles = [ion_particle, ion_particle] coulomb_log_val = Coulomb_logarithm(T_i, n_i, particles, V, method=coulomb_log_method) Z_i = atomic.integer_charge(ion_particle) # this is the same as b_perp in collisions.py, using most probable thermal velocity for V # and using ion mass instead of reduced mass bperp = (Z_i * e)**2 / (4 * np.pi * eps0 * m_i * V**2) # collisional cross-section sigma = np.pi * (2 * bperp)**2 # collisional frequency with Coulomb logarithm to correct for small angle collisions nu = n_i * sigma * V * coulomb_log_val # this coefficient is the constant that pops out when comparing this definition of # collisional frequency to the one in collisions.py coeff = np.sqrt(8 / np.pi) / 3 # collisional frequency modified by the constant difference nu_i = coeff * nu return nu_i.to(1 / u.s)