def __getitem__(self, value) -> State: """Return the ionic fraction(s).""" if isinstance(value, slice): raise TypeError("IonizationState instances cannot be sliced.") if isinstance(value, (int, np.integer)) and 0 <= value <= self.atomic_number: result = State(value, self.ionic_fractions[value], self.ionic_symbols[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]) else: if not same_element or not same_isotope: raise AtomicError("Inconsistent element or isotope.") elif not has_charge_info: raise ChargeError("No integer charge provided.") return result
def test_Particle_cmp(): """Test ``__eq__`` and ``__ne__`` in the Particle class.""" proton1 = Particle('p+') proton2 = Particle('proton') electron = Particle('e-') assert proton1 == proton2, "Particle('p+') == Particle('proton') is False." assert proton1 != electron, "Particle('p+') == Particle('e-') is True." with pytest.raises(TypeError): electron == 1 with pytest.raises(AtomicError): electron == 'dfasdf'
def particles(self) -> List[Particle]: """ Return a list of the `~plasmapy.atomic.Particle` class instances. """ return [ Particle(self._particle.particle, Z=i) for i in range(self.atomic_number + 1) ]
def test_particle_class_mass_nuclide_mass(isotope: str, ion: str): """ Test that the ``mass`` and ``nuclide_mass`` attributes return equivalent values when appropriate. The inputs should generally be an isotope with no charge information, and a fully ionized ion of that isotope, in order to make sure that the nuclide mass of the isotope equals the mass of the fully ionized ion. This method may also check neutrons and protons. """ Isotope = Particle(isotope) Ion = Particle(ion) if Isotope.categories & {'isotope', 'baryon' } and Ion.categories & {'ion', 'baryon'}: particle = Isotope.particle assert Isotope.nuclide_mass == Ion.mass, ( f"Particle({repr(particle)}).nuclide_mass does not equal " f"Particle({repr(particle)}).mass") else: inputerrmsg = (f"isotope = {repr(isotope)} and ion = {repr(ion)} are " f"not valid inputs to this test. The inputs should be " f"an isotope with no charge information, and a fully " f"ionized ion of that isotope, in order to make sure " f"that the nuclide mass of the isotope equals the mass " f"of the ion.") assert Isotope.isotope and not Isotope.ion, inputerrmsg assert Isotope.isotope == Ion.isotope, inputerrmsg assert Ion.integer_charge == Ion.atomic_number, inputerrmsg assert Isotope.nuclide_mass == Ion.mass, ( f"The nuclide mass of {isotope} does not equal the mass of {ion} " f"which is the fully ionized ion of that isotope. The results of " f"the test are:\n\n" f"Particle({repr(ion)}).mass = {Ion.mass}\n" f"Particle({repr(isotope)}).nuclide_mass = {Isotope.nuclide_mass}" "\n")
def is_stable(particle: Particle, mass_numb: Optional[numbers.Integral] = None) -> bool: """ Return `True` for stable isotopes and particles and `False` for unstable isotopes. Parameters ---------- particle: `int`, `str`, or `~plasmapy.atomic.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 test_Particle_class(arg, kwargs, expected_dict): """ Test that `~plasmapy.atomic.Particle` objects for different subatomic particles, elements, isotopes, and ions return the expected properties. Provide a detailed error message that lists all of the inconsistencies with the expected results. """ call = call_string(Particle, arg, kwargs) errmsg = "" try: particle = Particle(arg, **kwargs) except Exception as exc: raise AtomicError(f"Problem creating {call}") from exc for key in expected_dict.keys(): expected = expected_dict[key] if inspect.isclass(expected) and issubclass(expected, Exception): # Exceptions are expected to be raised when accessing certain # attributes for some particles. For example, accessing a # neutrino's mass should raise a MissingAtomicDataError since # only upper limits of neutrino masses are presently available. # If expected_dict[key] is an exception, then check to make # sure that this exception is raised. try: with pytest.raises(expected): exec(f"particle.{key}") except pytest.fail.Exception: errmsg += f"\n{call}[{key}] does not raise {expected}." except Exception: errmsg += (f"\n{call}[{key}] does not raise {expected} but " f"raises a different exception.") else: try: result = eval(f"particle.{key}") assert result == expected except AssertionError: errmsg += (f"\n{call}.{key} returns {result} instead " f"of the expected value of {expected}.") except Exception: errmsg += f"\n{call}.{key} raises an unexpected exception." if len(errmsg) > 0: raise Exception(f"Problems with {call}:{errmsg}")
def test_particle_half_life_string(): """ Find the first isotope where the half-life is stored as a string (because the uncertainties are too great), and tests that requesting the half-life of that isotope causes a `MissingAtomicDataWarning` whilst returning a string. """ for isotope in known_isotopes(): half_life = _Isotopes[isotope].get('half-life', None) if isinstance(half_life, str): break with pytest.warns(MissingAtomicDataWarning): assert isinstance(Particle(isotope).half_life, str)
def process_particles_list( unformatted_particles_list: List[Union[str, Particle]]) \ -> List[Particle]: """ Take an unformatted list of particles and puts each particle into standard form, while allowing an integer and asterisk immediately preceding a particle to act as a multiplier. A string argument will be treated as a list containing that string as its sole item. """ if isinstance(unformatted_particles_list, str): unformatted_particles_list = [unformatted_particles_list] if not isinstance(unformatted_particles_list, (list, tuple)): raise TypeError("The input to process_particles_list should be a " "string, list, or tuple.") particles = [] for original_item in unformatted_particles_list: try: item = original_item.strip() if item.count('*') == 1 and item[0].isdigit(): multiplier_str, item = item.split('*') multiplier = int(multiplier_str) else: multiplier = 1 try: particle = Particle(item) except (InvalidParticleError) as exc: raise AtomicError(errmsg) from exc if particle.element and not particle.isotope: raise AtomicError(errmsg) [particles.append(particle) for i in range(multiplier)] except Exception: raise AtomicError( f"{original_item} is not a valid reactant or " "product in a nuclear reaction.") from None return particles
def get_particle_mass(particle) -> u.Quantity: """Return the mass of a particle. Take a representation of a particle and returns the mass in kg. If the input is a `~astropy.units.Quantity` or `~astropy.constants.Constant` with units of mass already, then this returns that mass converted to kg. """ try: if isinstance(particle, (u.Quantity, const.Constant)): return particle.to(u.kg) if not isinstance(particle, Particle): particle = Particle(particle) return particle.mass.to(u.kg) except u.UnitConversionError as exc1: raise u.UnitConversionError(f"Incorrect units in reduced_mass.") from exc1 except MissingAtomicDataError: raise MissingAtomicDataError( f"Unable to find the reduced mass because the mass of " f"{particle} is not available.") from None
def test_particleing_a_particle(arg): """ Test that Particle(arg) is equal to Particle(Particle(arg)), but is not the same object in memory. """ particle = Particle(arg) assert particle == Particle(particle), ( f"Particle({repr(arg)}) does not equal " f"Particle(Particle({repr(arg)}).") assert particle == Particle(Particle( Particle(particle))), (f"Particle({repr(arg)}) does not equal " f"Particle(Particle(Particle({repr(arg)})).") assert particle is not Particle(particle), ( f"Particle({repr(arg)}) is the same object in memory as " f"Particle(Particle({repr(arg)})), when it is intended to " f"create a new object in memory (e.g., a copy).")
def particle(request): return Particle(request.param)
def test_unary_operator_for_elements(): with pytest.raises(AtomicError): Particle('C').antiparticle
def test_antiparticle_inversion(particle, antiparticle): """Test that antiparticles have the correct antiparticles.""" assert Particle(antiparticle).antiparticle == Particle(particle), \ (f"The antiparticle of {antiparticle} is found to be " f"{~Particle(antiparticle)} instead of {particle}.")
def test_particle_bool_error(): with pytest.raises(AtomicError): bool(Particle('e-'))
def ionic_fractions(self, inputs: Union[Dict, List, Tuple]): """Set the ionic fractions""" if isinstance(inputs, dict): original_keys = inputs.keys() ionfrac_types = {type(inputs[key]) for key in original_keys} if u.Quantity in ionfrac_types 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.") # Create a dictionary of Particle instances particles = dict() for key in original_keys: try: particles[key] = key if isinstance( key, Particle) else Particle(key) except (InvalidParticleError, TypeError) as exc: raise AtomicError( f"Unable to create IonizationStates instance " f"because {key} is not a valid particle") from exc # The particles whose ionization states are to be recorded # should be elements or isotopes but not ions or neutrals. 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 AtomicError( 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. sorted_keys = sorted(original_keys, key=lambda k: ( particles[k].atomic_number, particles[k].mass_number if particles[k].isotope else 0, )) _elements = [] _particles = [] new_ionic_fractions = {} for key in sorted_keys: new_key = particles[key].particle _particles.append(particles[key]) if new_key in _elements: raise AtomicError( "Repeated particles in IonizationStates.") _elements.append(new_key) if isinstance(inputs[key], u.Quantity): 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) except u.UnitConversionError as exc: raise AtomicError( "Units are not inverse volume.") from exc elif isinstance(inputs[key], np.ndarray) and inputs[key].dtype.kind == 'f': new_ionic_fractions[particles[key].particle] = inputs[key] else: try: new_ionic_fractions[particles[key].particle] = \ np.array(inputs[key], dtype=np.float) except ValueError: raise AtomicError( f"Inappropriate ionic fractions for {key}.") for key in _elements: if np.min(new_ionic_fractions[key]) < 0 or np.max( new_ionic_fractions[key]) > 1: raise AtomicError( f"Ionic fractions for {key} are not between 0 and 1.") if not np.isclose( np.sum(new_ionic_fractions[key]), 1, atol=self.tol, rtol=0): raise AtomicError( f"Ionic fractions for {key} are not normalized to 1.") elif isinstance(inputs, (list, tuple)): try: _particles = [Particle(particle) for particle in inputs] except (InvalidParticleError, TypeError): raise AtomicError("Invalid inputs to IonizationStates") _particles.sort(key=lambda p: (p.atomic_number, p.mass_number if p.isotope else 0)) _elements = [particle.particle for particle in _particles] new_ionic_fractions = { particle.particle: np.full(particle.atomic_number + 1, fill_value=np.nan, dtype=np.float64) for particle in _particles } else: raise TypeError # Because this depends on _particles being sorted, we add in an # easy check that atomic numbers do not decrease. for i in range(1, len(_particles)): if _particles[i - 1].element == _particles[i].element: if not _particles[i - 1].isotope and _particles[i].isotope: raise AtomicError( "Cannot have an element and isotopes of that element.") elif _particles[i - 1].atomic_number > _particles[i].atomic_number: raise AtomicError("_particles has not been sorted.") self._particles = _particles self._elements = _elements self._ionic_fractions = new_ionic_fractions
'particle': 'He-4 2+', 'element': 'He', 'element_name': 'helium', 'isotope': 'He-4', 'isotope_name': 'helium-4', 'ionic_symbol': 'He-4 2+', 'roman_symbol': 'He-4 III', 'mass_energy': 5.971919969131517e-10 * u.J, 'is_ion': True, 'integer_charge': 2, 'atomic_number': 2, 'mass_number': 4, 'baryon_number': 4, 'lepton_number': 0, 'half_life': np.inf * u.s, 'recombine()': Particle('He-4 1+') }), ('He-4 0+', {}, { 'particle': 'He-4 0+', 'element': 'He', 'isotope': 'He-4', 'mass_energy': 5.971919969131517e-10 * u.J, }), ('Li', { 'mass_numb': 7 }, { 'particle': 'Li-7', 'element': 'Li', 'element_name': 'lithium', 'isotope': 'Li-7', 'isotope_name': 'lithium-7',
{'particle': 'He-4 2+', 'element': 'He', 'element_name': 'helium', 'isotope': 'He-4', 'isotope_name': 'helium-4', 'ionic_symbol': 'He-4 2+', 'roman_symbol': 'He-4 III', 'mass_energy': 5.971919969131517e-10 * u.J, 'is_ion': True, 'integer_charge': 2, 'atomic_number': 2, 'mass_number': 4, 'baryon_number': 4, 'lepton_number': 0, 'half_life': np.inf * u.s, 'recombine()': Particle('He-4 1+') }), ('He-4 0+', {}, {'particle': 'He-4 0+', 'element': 'He', 'isotope': 'He-4', 'mass_energy': 5.971919969131517e-10 * u.J, }), ('Li', {'mass_numb': 7}, {'particle': 'Li-7', 'element': 'Li', 'element_name': 'lithium', 'isotope': 'Li-7', 'isotope_name': 'lithium-7',