From ba45c40d7acfa3ae89ef6cdc7bea85e072e45870 Mon Sep 17 00:00:00 2001 From: Nick Murphy Date: Thu, 24 Feb 2022 16:38:04 -0500 Subject: [PATCH] Fix bug with proton equality while continuing `Particle.__init__` refactoring (#1366) * Add failing test for proton equality bug * Add failing test for Particle("H", Z=1, mass_numb=1) * Add a hack to fix proton equality bug * A hack to try to fix This bug suggests that Particle.__init__ could use some friendly refactoring. * Add changelog entry * More extract method pattern from Particle.__init__ * Create inputs dict in Particle * Use Particle._inputs instead of arguments to methods * More extract method * Move method around * Minor reorganizing * More re-organizing * Rename variable * Use _inputs instead of arguments * Finally proton equality bug * Renaming and reorganization * Simplify naming * Simplify naming * Use _inputs * Minor changes * Minor changes * Minor changes * Minor changes * Minor changes * Renaming variables and some minor reorganization * Renaming variables and reorganization * Slight change to test * Slight change to test * Slight changes to tests * Add second changelog entry * Update docstrings * Remove Particle.__name__ * Remove unnecessary .keys() for dicts * Remove unnecessary .keys() for dicts * Change Particle._inputs to Particle.__inputs * Rename dict containing data about special particles * Remove delattr of Particle.__inputs * Remove unnecessary .keys() for dicts * Remove unnecessary use of .keys() in tests * Remove unnecessary import * Add docstring back for Particle.__init__ Adding this line back because it might be responsible for some documentation test failures that started happening soon after. * Add back Particle.__name__ * Remove docstring for Particle.__init__ * Revert Particle.__name__ line to pre-PR form * Add comment about Particle.__name__ * Updates following code review * Add comment about why protons are being treated specially --- changelog/1366.bugfix.rst | 2 + changelog/1366.trivial.rst | 2 + plasmapy/particles/atomic.py | 24 ++- plasmapy/particles/elements.py | 6 +- plasmapy/particles/isotopes.py | 2 +- plasmapy/particles/parsing.py | 37 ++-- plasmapy/particles/particle_class.py | 203 ++++++++++-------- plasmapy/particles/special_particles.py | 8 +- plasmapy/particles/tests/test_atomic.py | 8 +- .../tests/test_ionization_collection.py | 18 +- .../particles/tests/test_ionization_state.py | 5 +- .../particles/tests/test_particle_class.py | 57 ++++- .../particles/tests/test_special_particles.py | 21 +- 13 files changed, 242 insertions(+), 151 deletions(-) create mode 100644 changelog/1366.bugfix.rst create mode 100644 changelog/1366.trivial.rst diff --git a/changelog/1366.bugfix.rst b/changelog/1366.bugfix.rst new file mode 100644 index 0000000000..bf676e600a --- /dev/null +++ b/changelog/1366.bugfix.rst @@ -0,0 +1,2 @@ +Fixed a bug with |Particle| where ``Particle("p+") == Particle("H", Z=1, +mass_numb=1)`` led to a |ParticleError|. diff --git a/changelog/1366.trivial.rst b/changelog/1366.trivial.rst new file mode 100644 index 0000000000..67e4f5a6cf --- /dev/null +++ b/changelog/1366.trivial.rst @@ -0,0 +1,2 @@ +Used the extract method refactoring pattern on the initialization of +|Particle| objects. diff --git a/plasmapy/particles/atomic.py b/plasmapy/particles/atomic.py index 12d5892cc4..4eb14d3eed 100644 --- a/plasmapy/particles/atomic.py +++ b/plasmapy/particles/atomic.py @@ -28,14 +28,14 @@ from typing import Any, List, Optional, Union from plasmapy.particles.decorators import particle_input -from plasmapy.particles.elements import _elements +from plasmapy.particles.elements import _data_about_elements from plasmapy.particles.exceptions import ( InvalidElementError, InvalidIsotopeError, InvalidParticleError, MissingParticleDataError, ) -from plasmapy.particles.isotopes import _isotopes +from plasmapy.particles.isotopes import _data_about_isotopes from plasmapy.particles.particle_class import Particle from plasmapy.particles.symbols import atomic_symbol from plasmapy.utils.decorators.deprecation import deprecated @@ -590,7 +590,7 @@ def known_isotopes(argument: Union[str, Integral] = None) -> List[str]: def known_isotopes_for_element(argument): element = atomic_symbol(argument) isotopes = [] - for isotope in _isotopes.keys(): + for isotope in _data_about_isotopes: if element + "-" in isotope and isotope[0 : len(element)] == element: isotopes.append(isotope) if element == "H": @@ -616,7 +616,7 @@ def known_isotopes_for_element(argument): raise InvalidParticleError("Invalid particle in known_isotopes.") elif argument is None: isotopes_list = [] - for atomic_numb in range(1, len(_elements.keys()) + 1): + for atomic_numb in range(1, len(_data_about_elements) + 1): isotopes_list += known_isotopes_for_element(atomic_numb) return isotopes_list @@ -700,11 +700,13 @@ def common_isotopes_for_element( isotopes = known_isotopes(argument) CommonIsotopes = [ - isotope for isotope in isotopes if "abundance" in _isotopes[isotope].keys() + isotope + for isotope in isotopes + if "abundance" in _data_about_isotopes[isotope] ] isotopic_abundances = [ - _isotopes[isotope]["abundance"] for isotope in CommonIsotopes + _data_about_isotopes[isotope]["abundance"] for isotope in CommonIsotopes ] sorted_isotopes = [ @@ -818,7 +820,7 @@ def stable_isotopes_for_element( StableIsotopes = [ isotope for isotope in KnownIsotopes - if _isotopes[isotope]["stable"] == stable_only + if _data_about_isotopes[isotope]["stable"] == stable_only ] return StableIsotopes @@ -962,7 +964,7 @@ def periodic_table_period(argument: Union[str, Integral]) -> Integral: "integer representing its atomic number." ) symbol = atomic_symbol(argument) - period = _elements[symbol]["period"] + period = _data_about_elements[symbol]["period"] return period @@ -1016,7 +1018,7 @@ def periodic_table_group(argument: Union[str, Integral]) -> Integral: "symbol, or an integer representing its atomic number." ) symbol = atomic_symbol(argument) - group = _elements[symbol]["group"] + group = _data_about_elements[symbol]["group"] return group @@ -1070,7 +1072,7 @@ def periodic_table_block(argument: Union[str, Integral]) -> str: "symbol, or an integer representing its atomic number." ) symbol = atomic_symbol(argument) - block = _elements[symbol]["block"] + block = _data_about_elements[symbol]["block"] return block @@ -1122,7 +1124,7 @@ def periodic_table_category(argument: Union[str, Integral]) -> str: "symbol, or an integer representing its atomic number." ) symbol = atomic_symbol(argument) - category = _elements[symbol]["category"] + category = _data_about_elements[symbol]["category"] return category diff --git a/plasmapy/particles/elements.py b/plasmapy/particles/elements.py index 8c22907467..d291ed6398 100644 --- a/plasmapy/particles/elements.py +++ b/plasmapy/particles/elements.py @@ -45,16 +45,16 @@ def _element_obj_hook(obj): # json.dump(_Elements, f, default=plasma_default, indent=2) -_elements = json.loads( +_data_about_elements = json.loads( pkgutil.get_data("plasmapy", "particles/data/elements.json"), object_hook=_element_obj_hook, ) _atomic_numbers_to_symbols = { - elemdict["atomic number"]: symb for (symb, elemdict) in _elements.items() + elemdict["atomic number"]: symb for (symb, elemdict) in _data_about_elements.items() } _element_names_to_symbols = { - elemdict["element name"]: symb for (symb, elemdict) in _elements.items() + elemdict["element name"]: symb for (symb, elemdict) in _data_about_elements.items() } diff --git a/plasmapy/particles/isotopes.py b/plasmapy/particles/isotopes.py index 39aa8dc87f..bc81f8cb72 100644 --- a/plasmapy/particles/isotopes.py +++ b/plasmapy/particles/isotopes.py @@ -32,7 +32,7 @@ def _isotope_obj_hook(obj): #: Dictionary of isotope data. -_isotopes = json.loads( +_data_about_isotopes = json.loads( pkgutil.get_data("plasmapy", "particles/data/isotopes.json"), object_hook=_isotope_obj_hook, ) diff --git a/plasmapy/particles/parsing.py b/plasmapy/particles/parsing.py index fa09c23f6e..93f588a1f0 100644 --- a/plasmapy/particles/parsing.py +++ b/plasmapy/particles/parsing.py @@ -16,20 +16,23 @@ from plasmapy.particles.elements import ( _atomic_numbers_to_symbols, + _data_about_elements, _element_names_to_symbols, - _elements, ) from plasmapy.particles.exceptions import ( InvalidElementError, InvalidParticleError, ParticleWarning, ) -from plasmapy.particles.isotopes import _isotopes -from plasmapy.particles.special_particles import _Particles, ParticleZoo +from plasmapy.particles.isotopes import _data_about_isotopes +from plasmapy.particles.special_particles import ( + _data_about_special_particles, + ParticleZoo, +) from plasmapy.utils import roman -def _create_alias_dicts(Particles: dict) -> (Dict[str, str], Dict[str, str]): +def _create_alias_dicts(particles: dict) -> (Dict[str, str], Dict[str, str]): """ Create dictionaries for case sensitive aliases and case insensitive aliases of special particles and antiparticles. @@ -42,8 +45,8 @@ def _create_alias_dicts(Particles: dict) -> (Dict[str, str], Dict[str, str]): case_sensitive_aliases = {} case_insensitive_aliases = {} - for symbol in Particles.keys(): - name = Particles[symbol]["name"] + for symbol in particles: + name = particles[symbol]["name"] case_insensitive_aliases[name.lower()] = symbol case_sensitive_aliases_for_a_symbol = [ @@ -111,7 +114,9 @@ def _create_alias_dicts(Particles: dict) -> (Dict[str, str], Dict[str, str]): return case_sensitive_aliases, case_insensitive_aliases -_case_sensitive_aliases, _case_insensitive_aliases = _create_alias_dicts(_Particles) +_case_sensitive_aliases, _case_insensitive_aliases = _create_alias_dicts( + _data_about_special_particles +) def _dealias_particle_aliases(alias: Union[str, Integral]) -> str: @@ -129,9 +134,9 @@ def _dealias_particle_aliases(alias: Union[str, Integral]) -> str: or alias in _case_insensitive_aliases.values() ): symbol = alias - elif alias in _case_sensitive_aliases.keys(): + elif alias in _case_sensitive_aliases: symbol = _case_sensitive_aliases[alias] - elif alias.lower() in _case_insensitive_aliases.keys(): + elif alias.lower() in _case_insensitive_aliases: symbol = _case_insensitive_aliases[alias.lower()] else: symbol = alias @@ -269,7 +274,7 @@ def _atomic_number_to_symbol(atomic_numb: Integral): `~plasmapy.particles.exceptions.InvalidParticleError` if the atomic number does not represent a known element. """ - if atomic_numb in _atomic_numbers_to_symbols.keys(): + if atomic_numb in _atomic_numbers_to_symbols: return _atomic_numbers_to_symbols[atomic_numb] else: raise InvalidParticleError(f"{atomic_numb} is not a valid atomic number.") @@ -311,7 +316,7 @@ def _get_element(element_info: str) -> str: Receive a `str` representing an element's symbol or name, and returns a `str` representing the atomic symbol. """ - if element_info.lower() in _element_names_to_symbols.keys(): + if element_info.lower() in _element_names_to_symbols: element = _element_names_to_symbols[element_info.lower()] elif element_info in _atomic_numbers_to_symbols.values(): element = element_info @@ -339,7 +344,7 @@ def _reconstruct_isotope_symbol(element: str, mass_numb: Integral) -> str: elif isotope == "H-3": isotope = "T" - if isotope not in _isotopes.keys(): + if isotope not in _data_about_isotopes: raise InvalidParticleError( f"The string '{isotope}' does not correspond to " f"a valid isotope." @@ -440,10 +445,10 @@ def _reconstruct_ion_symbol( Z = Z_from_arg if isinstance(Z, Integral): - if Z > _elements[element]["atomic number"]: + if Z > _data_about_elements[element]["atomic number"]: raise InvalidParticleError( f"The charge number Z = {Z} cannot exceed the atomic number " - f"of {element}, which is {_elements[element]['atomic number']}." + f"of {element}, which is {_data_about_elements[element]['atomic number']}." ) elif Z <= -3: warnings.warn( @@ -463,7 +468,7 @@ def _reconstruct_ion_symbol( else: symbol = element - nomenclature_dict = { + return { "symbol": symbol, "element": element, "isotope": isotope, @@ -472,8 +477,6 @@ def _reconstruct_ion_symbol( "charge number": Z, } - return nomenclature_dict - def _parse_and_check_molecule_input(argument: str, Z: Integral = None): """ diff --git a/plasmapy/particles/particle_class.py b/plasmapy/particles/particle_class.py index 990932e7ef..dd60aa23cf 100644 --- a/plasmapy/particles/particle_class.py +++ b/plasmapy/particles/particle_class.py @@ -24,7 +24,8 @@ from numbers import Integral, Real from typing import Iterable, List, Optional, Set, Tuple, Union -from plasmapy.particles.elements import _elements, _PeriodicTable +from plasmapy.particles import parsing +from plasmapy.particles.elements import _data_about_elements, _PeriodicTable from plasmapy.particles.exceptions import ( ChargeError, InvalidElementError, @@ -36,16 +37,10 @@ ParticleError, ParticleWarning, ) -from plasmapy.particles.isotopes import _isotopes -from plasmapy.particles.parsing import ( - _dealias_particle_aliases, - _invalid_particle_errmsg, - _parse_and_check_atomic_input, - _parse_and_check_molecule_input, -) +from plasmapy.particles.isotopes import _data_about_isotopes from plasmapy.particles.special_particles import ( _antiparticles, - _Particles, + _data_about_special_particles, _special_ion_masses, ParticleZoo, ) @@ -423,9 +418,42 @@ class Particle(AbstractPhysicalParticle): ``'transition metal'``, ``'uncharged'``, and ``'unstable'``. """ - @staticmethod - def _validate_arguments(argument, Z, mass_numb): + def __init__( + self, + argument: ParticleLike, + mass_numb: Integral = None, + Z: Integral = None, + ): + + # If argument is a Particle instance, then construct a new + # Particle instance for the same particle. + + if isinstance(argument, Particle): + argument = argument.symbol + + self.__inputs = argument, mass_numb, Z + + self._initialize_attributes_and_categories() + self._store_particle_identity() + self._assign_particle_attributes() + self._add_charge_information() + self._add_half_life_information() + + # If __name__ is not defined here, then problems with the doc + # build arise related to the Particle instances that are + # defined in plasmapy/particles/__init__.py. + + self.__name__ = self.__repr__() + + def _initialize_attributes_and_categories(self): + """Create empty collections for attributes and categories.""" + self._attributes = defaultdict(type(None)) + self._categories = set() + + def _validate_inputs(self): """Raise appropriate exceptions when inputs are invalid.""" + argument, mass_numb, Z = self.__inputs + if not isinstance(argument, (Integral, np.integer, str, Particle)): raise TypeError( "The first positional argument when creating a " @@ -439,37 +467,77 @@ def _validate_arguments(argument, Z, mass_numb): if Z is not None and not isinstance(Z, Integral): raise TypeError("Z is not an integer.") - def _initialize_attrs_categories(self): - """Create empty collections for attributes & categories.""" + def _store_particle_identity(self): + """Store the particle's symbol and identifying information.""" + self._validate_inputs() + argument, mass_numb, Z = self.__inputs + symbol = parsing._dealias_particle_aliases(argument) + if symbol in _data_about_special_particles: + self._attributes["symbol"] = symbol + else: + self._store_identity_of_atom(argument) - self._attributes = defaultdict(type(None)) - self._categories = set() + def _store_identity_of_atom(self, argument): + """ + Store the particle's symbol, element, isotope, ion, mass number, + and charge number. + """ + _, mass_numb, Z = self.__inputs + + try: + information_about_atom = parsing._parse_and_check_atomic_input( + argument, + mass_numb=mass_numb, + Z=Z, + ) + except Exception as exc: + errmsg = parsing._invalid_particle_errmsg( + argument, mass_numb=mass_numb, Z=Z + ) + raise InvalidParticleError(errmsg) from exc - def _initialize_special_particle(self, particle_symbol, Z, mass_numb): + self._attributes["symbol"] = information_about_atom["symbol"] + + for key in information_about_atom: + self._attributes[key] = information_about_atom[key] + + def _assign_particle_attributes(self): + """Assign particle attributes and categories.""" + if self.symbol in _data_about_special_particles: + self._assign_special_particle_attributes() + else: + self._assign_atom_attributes() + + def _assign_special_particle_attributes(self): """Initialize special particles.""" attributes = self._attributes categories = self._categories - attributes["symbol"] = particle_symbol - - for attribute in _Particles[particle_symbol].keys(): - attributes[attribute] = _Particles[particle_symbol][attribute] + for attribute in _data_about_special_particles[self.symbol]: + attributes[attribute] = _data_about_special_particles[self.symbol][ + attribute + ] particle_taxonomy = ParticleZoo._taxonomy_dict all_categories = particle_taxonomy.keys() for category in all_categories: - if particle_symbol in particle_taxonomy[category]: + if self.symbol in particle_taxonomy[category]: categories.add(category) if attributes["name"] in _specific_particle_categories: categories.add(attributes["name"]) - if particle_symbol == "p+": + # Protons are treated specially because they can be considered + # both as special particles and atomic particles. + + if self.symbol == "p+": categories.update({"element", "isotope", "ion"}) + _, mass_numb, Z = self.__inputs + if mass_numb is not None or Z is not None: - if particle_symbol == "p+" and (mass_numb == 1 or Z == 1): + if self.symbol == "p+" and (mass_numb == 1 or Z == 1): warnings.warn( "Redundant mass number or charge information.", ParticleWarning ) @@ -481,22 +549,11 @@ def _initialize_special_particle(self, particle_symbol, Z, mass_numb): f"use: Particle({repr(attributes['particle'])})" ) - def _initialize_atom(self, argument, Z, mass_numb): - """Assign attributes and categories to atoms.""" + def _assign_atom_attributes(self): + """Assign attributes and categories to elements, isotopes, and ions.""" attributes = self._attributes categories = self._categories - try: - nomenclature = _parse_and_check_atomic_input( - argument, mass_numb=mass_numb, Z=Z - ) - except Exception as exc: - errmsg = _invalid_particle_errmsg(argument, mass_numb=mass_numb, Z=Z) - raise InvalidParticleError(errmsg) from exc - - for key in nomenclature.keys(): - attributes[key] = nomenclature[key] - element = attributes["element"] isotope = attributes["isotope"] ion = attributes["ion"] @@ -510,10 +567,10 @@ def _initialize_atom(self, argument, Z, mass_numb): # Element properties - Element = _elements[element] + this_element = _data_about_elements[element] - attributes["atomic number"] = Element["atomic number"] - attributes["element name"] = Element["element name"] + attributes["atomic number"] = this_element["atomic number"] + attributes["element name"] = this_element["element name"] # Set the lepton number to zero for elements, isotopes, and # ions. The lepton number will probably come up primarily @@ -523,31 +580,31 @@ def _initialize_atom(self, argument, Z, mass_numb): if isotope: - Isotope = _isotopes[isotope] + this_isotope = _data_about_isotopes[isotope] - attributes["baryon number"] = Isotope["mass number"] - attributes["isotope mass"] = Isotope.get("mass", None) - attributes["isotopic abundance"] = Isotope.get("abundance", 0.0) + attributes["baryon number"] = this_isotope["mass number"] + attributes["isotope mass"] = this_isotope.get("mass", None) + attributes["isotopic abundance"] = this_isotope.get("abundance", 0.0) - if Isotope["stable"]: + if this_isotope["stable"]: attributes["half-life"] = np.inf * u.s else: - attributes["half-life"] = Isotope.get("half-life", None) + attributes["half-life"] = this_isotope.get("half-life", None) if element and not isotope: - attributes["standard atomic weight"] = Element.get("atomic mass", None) + attributes["standard atomic weight"] = this_element.get("atomic mass", None) - if ion in _special_ion_masses.keys(): + if ion in _special_ion_masses: attributes["mass"] = _special_ion_masses[ion] attributes["periodic table"] = _PeriodicTable( - group=Element["group"], - period=Element["period"], - block=Element["block"], - category=Element["category"], + group=this_element["group"], + period=this_element["period"], + block=this_element["block"], + category=this_element["category"], ) - categories.add(Element["category"]) + categories.add(this_element["category"]) def _add_charge_information(self): """Assign attributes and categories related to charge information.""" @@ -571,38 +628,6 @@ def _add_half_life_information(self): else: self._categories.add("unstable") - def __init__( - self, - argument: ParticleLike, - mass_numb: Integral = None, - Z: Integral = None, - ): - """Instantiate a |Particle| object and set private attributes.""" - - # If argument is a Particle instance, then we will construct a - # new Particle instance for the same Particle (essentially a - # copy). - - if isinstance(argument, Particle): - argument = argument.symbol - - self._validate_arguments(argument, Z=Z, mass_numb=mass_numb) - self._initialize_attrs_categories() - - particle_symbol = _dealias_particle_aliases(argument) - - is_special_particle = particle_symbol in _Particles.keys() - - if is_special_particle: - self._initialize_special_particle(particle_symbol, Z=Z, mass_numb=mass_numb) - else: - self._initialize_atom(particle_symbol, Z=Z, mass_numb=mass_numb) - - self._add_charge_information() - self._add_half_life_information() - - self.__name__ = self.__repr__() - def __repr__(self) -> str: """ Return a call string that would recreate this object. @@ -665,10 +690,10 @@ def __eq__(self, other) -> bool: # TODO: create function in utils to account for equality between # defaultdicts, and implement it here - for attribute in self._attributes.keys(): + for attribute in self._attributes: other._attributes[attribute] - for attribute in other._attributes.keys(): + for attribute in other._attributes: self._attributes[attribute] same_attributes = self._attributes == other._attributes @@ -770,7 +795,7 @@ def antiparticle(self) -> Particle: >>> ~antineutron Particle("n") """ - if self.symbol in _antiparticles.keys(): + if self.symbol in _antiparticles: return Particle(_antiparticles[self.symbol]) else: raise ParticleError( @@ -2301,7 +2326,9 @@ def molecule( try: return Particle(symbol, Z=Z) except ParticleError: - element_dict, bare_symbol, Z = _parse_and_check_molecule_input(symbol, Z) + element_dict, bare_symbol, Z = parsing._parse_and_check_molecule_input( + symbol, Z + ) mass = 0 * u.kg for element_symbol, amount in element_dict.items(): try: diff --git a/plasmapy/particles/special_particles.py b/plasmapy/particles/special_particles.py index f2fe842f0e..82a1b857da 100644 --- a/plasmapy/particles/special_particles.py +++ b/plasmapy/particles/special_particles.py @@ -251,11 +251,11 @@ def _create_Particles_dict() -> Dict[str, dict]: }, } - for particle in special_attributes.keys(): + for particle in special_attributes: Particles[particle] = {**special_attributes[particle], **Particles[particle]} for particle in ParticleZoo.everything: - if "half-life" not in Particles[particle].keys(): + if "half-life" not in Particles[particle]: Particles[particle]["half-life"] = np.inf * u.s for particle in ParticleZoo.particles: @@ -267,7 +267,7 @@ def _create_Particles_dict() -> Dict[str, dict]: return Particles -_Particles = _create_Particles_dict() +_data_about_special_particles = _create_Particles_dict() _special_ion_masses = { "p+": const.m_p, @@ -298,4 +298,4 @@ def _create_Particles_dict() -> Dict[str, dict]: from pprint import pprint print("Particles:") - pprint(_Particles) + pprint(_data_about_special_particles) diff --git a/plasmapy/particles/tests/test_atomic.py b/plasmapy/particles/tests/test_atomic.py index dae299f780..de46457ac9 100644 --- a/plasmapy/particles/tests/test_atomic.py +++ b/plasmapy/particles/tests/test_atomic.py @@ -38,7 +38,7 @@ stable_isotopes, standard_atomic_weight, ) -from ..isotopes import _isotopes +from ..isotopes import _data_about_isotopes from ..nuclear import nuclear_binding_energy, nuclear_reaction_energy from ..symbols import atomic_symbol, element_name, isotope_symbol @@ -383,10 +383,10 @@ def test_half_life(): def test_half_life_unstable_isotopes(): """Test that `half_life` returns `None` and raises an exception for all isotopes that do not yet have half-life data.""" - for isotope in _isotopes.keys(): + for isotope in _data_about_isotopes: if ( - "half_life" not in _isotopes[isotope].keys() - and not _isotopes[isotope].keys() + "half_life" not in _data_about_isotopes[isotope] + and not _data_about_isotopes[isotope] ): with pytest.raises(MissingParticleDataError): diff --git a/plasmapy/particles/tests/test_ionization_collection.py b/plasmapy/particles/tests/test_ionization_collection.py index 0ffc217672..a3da177ae1 100644 --- a/plasmapy/particles/tests/test_ionization_collection.py +++ b/plasmapy/particles/tests/test_ionization_collection.py @@ -35,8 +35,7 @@ def check_abundances_consistency( f"log_abundances.keys(): {log_abundances.keys()}" ) - for element in abundances.keys(): - abundance_from_abundances = abundances[element] + for element, abundance_from_abundances in abundances.items(): abundance_from_log_abundances = 10 ** log_abundances[element] assert np.isclose( abundance_from_abundances, abundance_from_log_abundances @@ -44,7 +43,7 @@ def check_abundances_consistency( def has_attribute(attribute, tests_dict): - cases = [test for test in tests_dict.keys() if attribute in tests_dict[test].keys()] + cases = [test for test in tests_dict if attribute in tests_dict[test]] if cases: return cases else: @@ -162,7 +161,7 @@ def test_that_particles_were_set_correctly(self, test_name): particles = [Particle(input_particle) for input_particle in input_particles] expected_particles = {p.symbol for p in particles} actual_particles = { - particle for particle in self.instances[test_name].ionic_fractions.keys() + particle for particle in self.instances[test_name].ionic_fractions } assert actual_particles == expected_particles, ( @@ -548,7 +547,7 @@ def test_setting_ionic_fractions_for_single_element(self): assert np.allclose( self.new_fractions, resulting_fractions ), "Ionic fractions for H not set using __setitem__." - assert "He" in self.instance.ionic_fractions.keys(), ( + assert "He" in self.instance.ionic_fractions, ( "He is missing in ionic_fractions after __setitem__ was " "used to set H ionic fractions." ) @@ -588,8 +587,7 @@ def test_setting_abundances(self): new_abundances = {"H": 1, "He": 0.1, "Li": 1e-4, "Fe": 1e-5, "Au": 1e-8} log_new_abundances = { - element: np.log10(new_abundances[element]) - for element in new_abundances.keys() + element: np.log10(new_abundances[element]) for element in new_abundances } try: @@ -724,7 +722,7 @@ def test_ionic_fractions_not_quantities(self, element): ) def test_that_iron_ionic_fractions_are_still_undefined(self): - assert "Fe" in self.instance.ionic_fractions.keys() + assert "Fe" in self.instance.ionic_fractions iron_fractions = self.instance.ionic_fractions["Fe"] assert len(iron_fractions) == atomic_number("Fe") + 1 assert np.all(np.isnan(iron_fractions)) @@ -761,7 +759,7 @@ def setup_class(cls): cls.n = 5.3 * u.m ** -3 cls.number_densities = { element: cls.ionic_fractions[element] * cls.n * cls.abundances[element] - for element in cls.ionic_fractions.keys() + for element in cls.ionic_fractions } # The keys that begin with 'ndens' have enough information to @@ -782,7 +780,7 @@ def setup_class(cls): cls.instances = { key: IonizationStateCollection(**cls.dict_of_kwargs[key]) - for key in cls.dict_of_kwargs.keys() + for key in cls.dict_of_kwargs } @pytest.mark.parametrize("test_key", ["ndens1", "ndens2"]) diff --git a/plasmapy/particles/tests/test_ionization_state.py b/plasmapy/particles/tests/test_ionization_state.py index 2a79409f59..9dba251a5a 100644 --- a/plasmapy/particles/tests/test_ionization_state.py +++ b/plasmapy/particles/tests/test_ionization_state.py @@ -17,7 +17,6 @@ from plasmapy.particles.particle_class import Particle from plasmapy.particles.particle_collections import ionic_levels, ParticleList from plasmapy.utils.exceptions import PlasmaPyFutureWarning -from plasmapy.utils.pytest_helpers import run_test ionic_fraction_table = [ ("Fe 6+", 0.52, 5.2e-6 * u.m ** -3), @@ -181,7 +180,7 @@ def test_integer_charges(self, test_name): @pytest.mark.parametrize( "test_name", - [name for name in test_names if "ionic_fractions" in test_cases[name].keys()], + [name for name in test_names if "ionic_fractions" in test_cases[name]], ) def test_ionic_fractions(self, test_name): """ @@ -371,7 +370,7 @@ def test_as_particle_list(self, test_name): @pytest.mark.parametrize( "test_name", - [name for name in test_names if "n_elem" in test_cases[name].keys()], + [name for name in test_names if "n_elem" in test_cases[name]], ) def test_electron_density_from_n_elem_ionic_fractions(self, test_name): instance = self.instances[test_name] diff --git a/plasmapy/particles/tests/test_particle_class.py b/plasmapy/particles/tests/test_particle_class.py index f190fb4d49..a5881c94be 100644 --- a/plasmapy/particles/tests/test_particle_class.py +++ b/plasmapy/particles/tests/test_particle_class.py @@ -22,7 +22,7 @@ ParticleError, ParticleWarning, ) -from plasmapy.particles.isotopes import _isotopes +from plasmapy.particles.isotopes import _data_about_isotopes from plasmapy.particles.particle_class import ( CustomParticle, DimensionlessParticle, @@ -98,6 +98,45 @@ "recombine()": "H-1 0+", }, ), + ( + "H", + {"Z": 1, "mass_numb": 1}, + { + "symbol": "p+", + "element": "H", + "element_name": "hydrogen", + "isotope": "H-1", + "isotope_name": "hydrogen-1", + "ionic_symbol": "p+", + "roman_symbol": "H-1 II", + "is_ion": True, + "mass": m_p, + "nuclide_mass": m_p, + "charge_number": 1, + "charge.value": e.si.value, + "spin": 1 / 2, + "half_life": np.inf * u.s, + "atomic_number": 1, + "mass_number": 1, + "lepton_number": 0, + "baryon_number": 1, + "__str__()": "p+", + "__repr__()": 'Particle("p+")', + 'is_category("fermion")': True, + 'is_category(["fermion"])': True, + 'is_category({"fermion"})': True, + 'is_category(any_of=("boson", "fermion"))': True, + 'is_category(require=["boson", "fermion"])': False, + 'is_category(("element", "isotope", "ion"))': True, + 'is_category("charged")': True, + "periodic_table.group": 1, + "periodic_table.block": "s", + "periodic_table.period": 1, + "periodic_table.category": "nonmetal", + "binding_energy": 0 * u.J, + "recombine()": "H-1 0+", + }, + ), ( "p-", {}, @@ -518,7 +557,7 @@ def test_Particle_class(arg, kwargs, expected_dict): except Exception: errmsg += f"\n{call}.{key} raises an unexpected exception." - if len(errmsg) > 0: + if errmsg: raise Exception(f"Problems with {call}:{errmsg}") @@ -637,6 +676,18 @@ def test_Particle_cmp(): assert electron != "dfasdf" +@pytest.mark.parametrize("particle", ["p+", "D+", "T+", "alpha"]) +def test_particle_equality_special_nuclides(particle): + particle_from_string = Particle(particle) + particle_from_numbers = Particle( + particle_from_string.element_name, + Z=particle_from_string.charge_number, + mass_numb=particle_from_string.mass_number, + ) + assert particle_from_string == particle_from_numbers + assert particle_from_string._attributes == particle_from_numbers._attributes + + nuclide_mass_and_mass_equiv_table = [ ("n", "neutron"), ("p+", "proton"), @@ -710,7 +761,7 @@ def test_particle_half_life_string(): """ for isotope in known_isotopes(): - half_life = _isotopes[isotope].get("half-life", None) + half_life = _data_about_isotopes[isotope].get("half-life", None) if isinstance(half_life, str): break diff --git a/plasmapy/particles/tests/test_special_particles.py b/plasmapy/particles/tests/test_special_particles.py index 9c1e95dad5..7a8b600f87 100644 --- a/plasmapy/particles/tests/test_special_particles.py +++ b/plasmapy/particles/tests/test_special_particles.py @@ -1,6 +1,9 @@ import pytest -from plasmapy.particles.special_particles import _Particles, ParticleZoo +from plasmapy.particles.special_particles import ( + _data_about_special_particles, + ParticleZoo, +) particle_antiparticle_pairs = [ ("e-", "e+"), @@ -19,11 +22,11 @@ def test_particle_antiparticle_pairs(particle, antiparticle): """Test that particles and antiparticles have the same or exact opposite properties in the _Particles dictionary.""" - assert not _Particles[particle][ + assert not _data_about_special_particles[particle][ "antimatter" ], f"{particle} is incorrectly marked as antimatter." - assert _Particles[antiparticle][ + assert _data_about_special_particles[antiparticle][ "antimatter" ], f"{antiparticle} is incorrectly marked as matter." @@ -39,16 +42,20 @@ def test_particle_antiparticle_pairs(particle, antiparticle): for key in identical_keys: assert ( - _Particles[particle][key] == _Particles[antiparticle][key] + _data_about_special_particles[particle][key] + == _data_about_special_particles[antiparticle][key] ), f"{particle} and {antiparticle} do not have identical {key}." for key in opposite_keys: assert ( - _Particles[particle][key] == -_Particles[antiparticle][key] + _data_about_special_particles[particle][key] + == -_data_about_special_particles[antiparticle][key] ), f"{particle} and {antiparticle} do not have exact opposite {key}." if particle not in ["e-", "n"]: - assert _Particles[particle]["name"] == _Particles[antiparticle]["name"].replace( + assert _data_about_special_particles[particle][ + "name" + ] == _data_about_special_particles[antiparticle]["name"].replace( "anti", "" ), f"{particle} and {antiparticle} do not have same name except for 'anti'." @@ -74,7 +81,7 @@ def test__Particles_required_keys(particle): for key in required_keys: try: - _Particles[particle][key] + _data_about_special_particles[particle][key] except KeyError: missing_keys.append(key)