def test_ion_list(particle, min_charge, max_charge, expected_charge_numbers): """Test that inputs to ionic_levels are interpreted correctly.""" particle = Particle(particle) ions = ionic_levels(particle, min_charge, max_charge) np.testing.assert_equal(ions.charge_number, expected_charge_numbers) assert ions[0].element == particle.element if particle.is_category("isotope"): assert ions[0].isotope == particle.isotope
def __init__( self, particle: Particle, ionic_fractions=None, *, T_e: u.K = np.nan * u.K, T_i: u.K = None, kappa: Real = np.inf, n_elem: u.m**-3 = np.nan * u.m**-3, tol: Union[float, int] = 1e-15, ): """ Initialize an `~plasmapy.particles.ionization_state.IonizationState` instance. """ self._number_of_particles = particle.atomic_number + 1 if particle.is_ion or particle.is_category(require=("uncharged", "element")): if ionic_fractions is None: ionic_fractions = np.zeros(self._number_of_particles) ionic_fractions[particle.charge_number] = 1.0 particle = Particle( particle.isotope if particle.isotope else particle.element) else: raise ParticleError( "The ionic fractions must not be specified when " "the input particle to IonizationState is an ion.") self._particle = particle try: self.tol = tol self.T_e = T_e self.T_i = T_i self.kappa = kappa if (not np.isnan(n_elem) and isinstance(ionic_fractions, u.Quantity) and ionic_fractions.si.unit == u.m**-3): raise ParticleError( "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 ParticleError( f"Unable to create IonizationState object for {particle.symbol}." ) from exc
def is_stable(particle: Particle, mass_numb: Optional[Integral] = None) -> bool: """ Return `True` for stable isotopes and particles and `False` for unstable isotopes. Parameters ---------- particle: `int`, `str`, or `~plasmapy.particles.Particle` A string representing an isotope or particle, or an integer representing an atomic number. mass_numb: `int`, optional The mass number of the isotope. Returns ------- is_stable: `bool` `True` if the isotope is stable, `False` if it is unstable. Raises ------ `~plasmapy.utils.InvalidIsotopeError` If the arguments correspond to a valid element but not a valid isotope. `~plasmapy.utils.InvalidParticleError` If the arguments do not correspond to a valid particle. `TypeError` If the argument is not a `str` or `int`. `~plasmapy.utils.MissingAtomicDataError` If stability information is not available. Examples -------- >>> is_stable("H-1") True >>> is_stable("tritium") False >>> is_stable("e-") True >>> is_stable("tau+") False """ if particle.element and not particle.isotope: raise InvalidIsotopeError( "The input to is_stable must be either an isotope or a special particle." ) return particle.is_category('stable')
def __getitem__(self, value) -> IonicLevel: """Return information for a single ionization level.""" if isinstance(value, slice): return [ IonicLevel( ion=Particle(self.base_particle, Z=val), ionic_fraction=self.ionic_fractions[val], number_density=self.number_densities[val], T_i=self.T_i[val], ) for val in range(0, self._number_of_particles)[value] ] if isinstance(value, Integral) and 0 <= value <= self.atomic_number: result = IonicLevel( ion=Particle(self.base_particle, Z=value), ionic_fraction=self.ionic_fractions[value], number_density=self.number_densities[value], T_i=self.T_i[value], ) else: if not isinstance(value, Particle): try: value = Particle(value) except InvalidParticleError as exc: raise InvalidParticleError( f"{value} is not a valid charge number or 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.charge_number result = IonicLevel( ion=Particle(self.base_particle, Z=Z), ionic_fraction=self.ionic_fractions[Z], number_density=self.number_densities[Z], T_i=self.T_i[Z], ) else: if not same_element or not same_isotope: raise ParticleError("Inconsistent element or isotope.") elif not has_charge_info: raise ChargeError("No charge number provided.") return result
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 get_particle(argname, params, already_particle, funcname): argval, Z, mass_numb = params """ Convert the argument to a `~plasmapy.particles.particle_class.Particle` object if it is not already one. """ if not already_particle: if not isinstance(argval, (numbers.Integral, str, tuple, list)): raise TypeError( f"The argument {argname} to {funcname} must be " f"a string, an integer or a tuple or list of them " f"corresponding to an atomic number, or a " f"Particle object.") try: particle = Particle(argval, Z=Z, mass_numb=mass_numb) except InvalidParticleError as e: raise InvalidParticleError( _particle_errmsg(argname, argval, Z, mass_numb, funcname)) from e # We will need to do the same error checks whether or not the # argument is already an instance of the Particle class. if already_particle: particle = argval # If the name of the argument annotated with Particle in the # decorated function is element, isotope, or ion; then this # decorator should raise the appropriate exception when the # particle ends up not being an element, isotope, or ion. cat_table = [ ("element", particle.element, InvalidElementError), ("isotope", particle.isotope, InvalidIsotopeError), ("ion", particle.ionic_symbol, InvalidIonError), ] for category_name, category_symbol, CategoryError in cat_table: if argname == category_name and not category_symbol: raise CategoryError( f"The argument {argname} = {repr(argval)} to " f"{funcname} does not correspond to a valid " f"{argname}.") # Some functions require that particles be charged, or # at least that particles have charge information. _charge_number = particle._attributes["charge number"] must_be_charged = "charged" in require must_have_charge_info = set(any_of) == {"charged", "uncharged"} uncharged = _charge_number == 0 lacks_charge_info = _charge_number is None if must_be_charged and (uncharged or must_have_charge_info): raise ChargeError( f"A charged particle is required for {funcname}.") if must_have_charge_info and lacks_charge_info: raise ChargeError( f"Charge information is required for {funcname}.") # Some functions require particles that belong to more complex # classification schemes. Again, be sure to provide a # maximally useful error message. if not particle.is_category( require=require, exclude=exclude, any_of=any_of): raise ParticleError( _category_errmsg(particle, require, exclude, any_of, funcname)) return particle