Repository: PlasmaPy/PlasmaPy (python)

Issue:
Add mass_energy as a new atomic function and a Particle attribute
The nuclear binding energy of a nucleon may be found from the [`nuclear_binding_energy`](https://github.com/PlasmaPy/PlasmaPy/blob/e7912ef7a32487c8193a3f880874b40aa1e4cf7a/plasmapy/atomic/nuclear.py#L18) function of the `atomic` subpackage or with the [`binding_energy`](https://github.com/PlasmaPy/PlasmaPy/blob/e7912ef7a32487c8193a3f880874b40aa1e4cf7a/plasmapy/atomic/particle_class.py#L898) attribute from a [`Particle`](https://github.com/PlasmaPy/PlasmaPy/blob/e7912ef7a32487c8193a3f880874b40aa1e4cf7a/plasmapy/atomic/particle_class.py#L99) instance.  It would be really helpful to have a `mass_energy` function in [`nuclear.py`](https://github.com/PlasmaPy/PlasmaPy/blob/master/plasmapy/atomic/nuclear.py) as well as a `mass_energy` attribute of the Particle class in [`particle_class.py`](https://github.com/PlasmaPy/PlasmaPy/blob/master/plasmapy/atomic/particle_class.py).  The [`@particle_input`](https://github.com/PlasmaPy/PlasmaPy/blob/e7912ef7a32487c8193a3f880874b40aa1e4cf7a/plasmapy/atomic/particle_input.py#L66) decorator will likely be helpful so that the `mass_energy` function can instead just get the result from the appropriate `Particle` instance.  

Thanks!
-Nick

Pull request description:
adding mass_energy as attribute to Particle and nuclear mass energy t…
Change Log:
- Adding mass_energy as attribute to the Particle 
- Nuclear mass energy to the nuclear.py function

  closes #294 




Patch:
diff --git a/plasmapy/atomic/atomic.py b/plasmapy/atomic/atomic.py
index aa9d6daa..54ed55e3 100644
--- a/plasmapy/atomic/atomic.py
+++ b/plasmapy/atomic/atomic.py
@@ -1,6 +1,5 @@
 """Functions that retrieve or are related to elemental or isotopic data."""
 
-import warnings
 from typing import (
     Union,
     Optional,
@@ -21,7 +20,6 @@
     InvalidParticleError,
     InvalidElementError,
     InvalidIsotopeError,
-    InvalidIonError,
     MissingAtomicDataError,
 )
 
diff --git a/plasmapy/atomic/nuclear.py b/plasmapy/atomic/nuclear.py
index 37dc75e4..16410112 100644
--- a/plasmapy/atomic/nuclear.py
+++ b/plasmapy/atomic/nuclear.py
@@ -1,6 +1,6 @@
 """Functions that are related to nuclear reactions."""
 
-from astropy import units as u, constants
+from astropy import units as u
 from typing import List, Union
 import re
 
@@ -15,6 +15,7 @@
 __all__ = [
     "nuclear_binding_energy",
     "nuclear_reaction_energy",
+    "mass_energy",
 ]
 
 
@@ -37,7 +38,7 @@ def nuclear_binding_energy(particle: Particle, mass_numb: int = None) -> u.Quant
     Returns
     -------
     binding_energy: `~astropy.units.Quantity`
-        The binding energy of the nucleus in units of Joules.
+        The binding energy of the nucleus in units of joules.
 
     Raises
     ------
@@ -52,9 +53,12 @@ def nuclear_binding_energy(particle: Particle, mass_numb: int = None) -> u.Quant
 
     See Also
     --------
-    `~plasmapy.atomic.nuclear_reaction_energy` : Returns the change in
+    `~plasmapy.atomic.nuclear_reaction_energy` : Return the change in
         binding energy during nuclear fusion or fission reactions.
 
+    `~plasmapy.atomic.mass_energy` : Return the mass energy of a
+        nucleon or particle.
+
     Examples
     --------
     >>> from astropy import units as u
@@ -74,6 +78,46 @@ def nuclear_binding_energy(particle: Particle, mass_numb: int = None) -> u.Quant
     return particle.binding_energy.to(u.J)
 
 
+@particle_input
+def mass_energy(particle: Particle, mass_numb: int = None) -> u.Quantity:
+    """
+    Return a particle's mass energy.  If the particle is an isotope or
+    nuclide, return the nuclear mass energy only.
+
+    Parameters
+    ----------
+    particle: `str`, `int`, or `~plasmapy.atomic.Particle`
+        A Particle object, a string representing an element or isotope,
+        or an integer representing the atomic number of an element.
+
+    mass_numb: `int` (optional)
+        The mass number of an isotope, which is required if and only
+        if the first argument can only be used to determine the
+        element and not the isotope.
+
+    Returns
+    -------
+    mass_energy: `~astropy.units.Quantity`
+        The mass energy of the particle (or in the case of ) in units of joules.
+
+    Raises
+    ------
+    `~plasmapy.utils.InvalidParticleError`
+        If the inputs do not correspond to a valid particle.
+
+    `~plasmapy.utils.AtomicError`
+        If the inputs do not correspond to a valid isotope or nucleon.
+
+    `TypeError`
+        If the inputs are not of the correct types.
+
+    >>> mass_energy('He-4')
+    <Quantity 5.97191997e-10 J>
+
+    """
+    return particle.mass_energy
+
+
 def nuclear_reaction_energy(*args, **kwargs):
     """
     Return the released energy from a nuclear reaction.
@@ -129,16 +173,19 @@ def nuclear_reaction_energy(*args, **kwargs):
     Examples
     --------
     >>> from astropy import units as u
+
     >>> nuclear_reaction_energy("D + T --> alpha + n")
     <Quantity 2.81812097e-12 J>
+
     >>> triple_alpha1 = '2*He-4 --> Be-8'
     >>> triple_alpha2 = 'Be-8 + alpha --> carbon-12'
     >>> energy_triplealpha1 = nuclear_reaction_energy(triple_alpha1)
     >>> energy_triplealpha2 = nuclear_reaction_energy(triple_alpha2)
     >>> print(energy_triplealpha1, energy_triplealpha2)
-    -1.4714307834388437e-14 J 1.18025735267267e-12 J
+    -1.4714307834388437e-14 J 1.1802573526724632e-12 J
     >>> energy_triplealpha2.to(u.MeV)
     <Quantity 7.36658704 MeV>
+
     >>> nuclear_reaction_energy(reactants=['n'], products=['p+', 'e-'])
     <Quantity 1.25343511e-13 J>
 
@@ -229,13 +276,10 @@ def add_mass_energy(particles: List[Particle]) -> u.Quantity:
         Find the total mass energy from a list of particles, while
         taking the masses of the fully ionized isotopes.
         """
-        total_mass = 0.0 * u.kg
+        total_mass_energy = 0.0 * u.J
         for particle in particles:
-            if particle.isotope:
-                total_mass += particle.nuclide_mass
-            else:
-                total_mass += particle.mass
-        return (total_mass * constants.c ** 2).to(u.J)
+            total_mass_energy += particle.mass_energy
+        return total_mass_energy.to(u.J)
 
     input_err_msg = (
         "The inputs to nuclear_reaction_energy should be either "
@@ -266,8 +310,8 @@ def add_mass_energy(particles: List[Particle]) -> u.Quantity:
 
         try:
             LHS_string, RHS_string = re.split('-+>', reaction)
-            LHS_list = re.split(' \+ ', LHS_string)
-            RHS_list = re.split(' \+ ', RHS_string)
+            LHS_list = re.split(r' \+ ', LHS_string)
+            RHS_list = re.split(r' \+ ', RHS_string)
             reactants = process_particles_list(LHS_list)
             products = process_particles_list(RHS_list)
         except Exception as ex:
diff --git a/plasmapy/atomic/parsing.py b/plasmapy/atomic/parsing.py
index c9744e3b..9ab4bee5 100644
--- a/plasmapy/atomic/parsing.py
+++ b/plasmapy/atomic/parsing.py
@@ -6,7 +6,11 @@
 from typing import (Union, Dict)
 import numbers
 
-from plasmapy.atomic.elements import (_atomic_numbers_to_symbols, _element_names_to_symbols, _Elements)
+from plasmapy.atomic.elements import (
+    _atomic_numbers_to_symbols,
+    _element_names_to_symbols,
+    _Elements)
+
 from plasmapy.atomic.isotopes import _Isotopes
 from plasmapy.atomic.special_particles import _Particles, ParticleZoo
 
diff --git a/plasmapy/atomic/particle_class.py b/plasmapy/atomic/particle_class.py
index ebf3853b..9e82bdee 100644
--- a/plasmapy/atomic/particle_class.py
+++ b/plasmapy/atomic/particle_class.py
@@ -276,7 +276,7 @@ class Particle:
     ``'post-transition metal'``, ``'proton'``, ``'stable'``,
     ``'transition metal'``, ``'uncharged'``, and ``'unstable'``.
 
-"""
+    """
 
     def __init__(
             self,
@@ -906,6 +906,129 @@ def mass(self) -> u.Quantity:
 
         raise MissingAtomicDataError(f"The mass of {self} is not available.")
 
+    @property
+    def nuclide_mass(self) -> u.Quantity:
+        """
+        Return the mass of the bare nucleus of an isotope or a neutron.
+
+        This attribute will raise a
+        `~plasmapy.utils.InvalidIsotopeError` if the particle is not an
+        isotope or neutron, or a
+        `~plasmapy.utils.MissingAtomicDataError` if the isotope mass is
+        not available.
+
+        Examples
+        --------
+        >>> deuterium = Particle('D')
+        >>> deuterium.nuclide_mass
+        <Quantity 3.34358372e-27 kg>
+
+        """
+
+        if self.isotope == 'H-1':
+            return const.m_p
+        elif self.isotope == 'D':
+            return _special_ion_masses['D 1+']
+        elif self.isotope == 'T':
+            return _special_ion_masses['T 1+']
+        elif self.particle == 'n':
+            return const.m_n
+
+        if not self.isotope:
+            raise InvalidIsotopeError(_category_errmsg(self, 'isotope'))
+
+        base_mass = self._attributes['isotope mass']
+
+        if base_mass is None:  # coverage: ignore
+            raise MissingAtomicDataError(f"The mass of a {self.isotope} nuclide is not available.")
+
+        _nuclide_mass = self._attributes['isotope mass'] - self.atomic_number * const.m_e
+
+        return _nuclide_mass.to(u.kg)
+
+    @property
+    def mass_energy(self) -> u.Quantity:
+        """
+        Return the mass energy of the particle in joules.
+
+        If the particle is an isotope or nuclide, return the mass energy
+        of the nucleus only.
+
+        If the mass of the particle is not known, then raise a
+        `~plasmapy.utils.MissingAtomicDataError`.
+
+        Examples
+        --------
+        >>> proton = Particle('p+')
+        >>> proton.mass_energy
+        <Quantity 1.50327759e-10 J>
+
+        >>> protium = Particle('H-1 0+')
+        >>> protium.mass_energy
+        <Quantity 1.50327759e-10 J>
+
+        >>> electron = Particle('electron')
+        >>> electron.mass_energy.to('MeV')
+        <Quantity 0.51099895 MeV>
+
+        """
+        try:
+            mass = self.nuclide_mass if self.isotope else self.mass
+            energy = mass * const.c ** 2
+            return energy.to(u.J)
+        except MissingAtomicDataError:
+            raise MissingAtomicDataError(
+                f"The mass energy of {self.particle} is not available "
+                f"because the mass is unknown.") from None
+
+    @property
+    def binding_energy(self) -> u.Quantity:
+        """
+        Return the nuclear binding energy in joules.
+
+        This attribute will raise an
+        `~plasmapy.utils.InvalidIsotopeError` if the particle is not a
+        nucleon or isotope.
+
+        Examples
+        --------
+        >>> alpha = Particle('alpha')
+        >>> alpha.binding_energy
+        <Quantity 4.53346938e-12 J>
+        >>> Particle('T').binding_energy.to('MeV')
+        <Quantity 8.48179621 MeV>
+
+        The binding energy of a nucleon equals 0 joules.
+
+        >>> neutron = Particle('n')
+        >>> proton = Particle('p+')
+        >>> neutron.binding_energy
+        <Quantity 0. J>
+        >>> proton.binding_energy
+        <Quantity 0. J>
+
+        """
+
+        if self._attributes['baryon number'] == 1:
+            return 0 * u.J
+
+        if not self.isotope:
+            raise InvalidIsotopeError(
+                f"The nuclear binding energy may only be calculated for nucleons and isotopes.")
+
+        number_of_protons = self.atomic_number
+        number_of_neutrons = self.mass_number - self.atomic_number
+
+        mass_of_protons = number_of_protons * const.m_p
+        mass_of_neutrons = number_of_neutrons * const.m_n
+
+        mass_of_nucleons = mass_of_protons + mass_of_neutrons
+
+        mass_defect = mass_of_nucleons - self.nuclide_mass
+        nuclear_binding_energy = mass_defect * const.c ** 2
+
+        return nuclear_binding_energy.to(u.J)
+
     @property
     def atomic_number(self) -> numbers.Integral:
         """
@@ -1086,45 +1209,6 @@ def lepton_number(self) -> numbers.Integral:
                 f"The lepton number for {self.particle} is not available.")
         return self._attributes['lepton number']
 
-    @property
-    def binding_energy(self) -> u.Quantity:
-        """
-        Return the nuclear binding energy in joules.
-
-        This attribute will raise an
-        `~plasmapy.utils.InvalidIsotopeError` if the particle is not a
-        nucleon or isotope.
-
-        Examples
-        --------
-        >>> alpha = Particle('alpha')
-        >>> alpha.binding_energy
-        <Quantity 4.53346938e-12 J>
-        >>> Particle('T').binding_energy.to('MeV')
-        <Quantity 8.48179621 MeV>
-
-        """
-
-        if self._attributes['baryon number'] == 1:
-            return 0 * u.J
-
-        if not self.isotope:
-            raise InvalidIsotopeError(
-                f"The nuclear binding energy may only be calculated for nucleons and isotopes.")
-
-        number_of_protons = self.atomic_number
-        number_of_neutrons = self.mass_number - self.atomic_number
-
-        mass_of_protons = number_of_protons * const.m_p
-        mass_of_neutrons = number_of_neutrons * const.m_n
-
-        mass_of_nucleons = mass_of_protons + mass_of_neutrons
-
-        mass_defect = mass_of_nucleons - self.nuclide_mass
-        nuclear_binding_energy = mass_defect * const.c ** 2
-
-        return nuclear_binding_energy.to(u.J)
-
     @property
     def half_life(self) -> Union[u.Quantity, str]:
         """
diff --git a/plasmapy/atomic/special_particles.py b/plasmapy/atomic/special_particles.py
index 69ab8a15..24539621 100644
--- a/plasmapy/atomic/special_particles.py
+++ b/plasmapy/atomic/special_particles.py
@@ -3,13 +3,12 @@
 information for special particles.
 """
 
-from typing import Set, Dict, List, Optional, Union
+from typing import Set, Dict
 from astropy import units as u, constants as const
 import numpy as np
 from plasmapy.atomic.elements import _PeriodicTable
 
 
-
 class _ParticleZooClass:
     """
     Create an object with taxonomy information for special particles.


Test patch:
diff --git a/plasmapy/atomic/tests/test_atomic.py b/plasmapy/atomic/tests/test_atomic.py
index 4aa338f6..e3fa18ca 100644
--- a/plasmapy/atomic/tests/test_atomic.py
+++ b/plasmapy/atomic/tests/test_atomic.py
@@ -414,6 +414,7 @@ def test_periodic_table_group(self):
         with pytest.raises(TypeError):
             periodic_table_group(('B', 'Ti', 'Ge'))
 
+
 # The tables above do not include the function to be tested in order to
 # avoid cluttering up the code.  The following block of code prepends
 # the correct function to each list containing args, kwargs, and the
diff --git a/plasmapy/atomic/tests/test_nuclear.py b/plasmapy/atomic/tests/test_nuclear.py
index 9dda788a..515e374c 100644
--- a/plasmapy/atomic/tests/test_nuclear.py
+++ b/plasmapy/atomic/tests/test_nuclear.py
@@ -1,8 +1,9 @@
-from astropy import units as u
+from astropy import units as u, constants as const
 import numpy as np
-from ..nuclear import nuclear_binding_energy, nuclear_reaction_energy
+from ..nuclear import nuclear_binding_energy, nuclear_reaction_energy, mass_energy
 from ...utils import (
     InvalidParticleError,
+    InvalidIsotopeError,
     AtomicError,
     run_test,
     run_test_equivalent_calls,
@@ -17,6 +18,11 @@
     [nuclear_binding_energy, 'He-99', {}, InvalidParticleError],
     [nuclear_binding_energy, "He", {"mass_numb": 99}, InvalidParticleError],
     [nuclear_binding_energy, 3.1415926535j, {}, TypeError],
+    [mass_energy, 'e-', {}, (const.m_e * const.c ** 2).to(u.J)],
+    [mass_energy, 'p+', {}, (const.m_p * const.c ** 2).to(u.J)],
+    [mass_energy, 'H-1', {}, (const.m_p * const.c ** 2).to(u.J)],
+    [mass_energy, 'H-1 0+', {}, (const.m_p * const.c ** 2).to(u.J)],
+    [mass_energy, 'n', {}, (const.m_n * const.c ** 2).to(u.J)],
     [nuclear_reaction_energy, (), {'reactants': ['n'], 'products': 3}, TypeError],
     [nuclear_reaction_energy, (), {'reactants': ['n'], 'products': ['He-4']}, AtomicError],
     [nuclear_reaction_energy, (), {'reactants': ['h'], 'products': ['H-1']}, AtomicError],
diff --git a/plasmapy/atomic/tests/test_particle_class.py b/plasmapy/atomic/tests/test_particle_class.py
index 45256541..59b282f1 100644
--- a/plasmapy/atomic/tests/test_particle_class.py
+++ b/plasmapy/atomic/tests/test_particle_class.py
@@ -4,7 +4,7 @@
 import inspect
 from plasmapy.utils import roman
 
-from ...constants import m_p, m_e, m_n, e
+from ...constants import m_p, m_e, m_n, e, c
 
 from ...utils import (
     AtomicWarning,
@@ -140,6 +140,7 @@
       'roman_symbol': None,
       'is_ion': False,
       'mass': m_e,
+      'mass_energy': (m_e * c ** 2).to('J'),
       'nuclide_mass': InvalidIsotopeError,
       'integer_charge': 1,
       'spin': 1 / 2,
@@ -293,6 +294,7 @@
       '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,
@@ -303,6 +305,13 @@
       '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',
@@ -335,7 +344,8 @@
       'mass_number': 276,
       'neutron_number': 164,
       'baryon_number': 276,
-      'lepton_number': 0}),
+      'lepton_number': 0,
+      }),
 
     ('muon', {},
      {'particle': 'mu-',
@@ -358,6 +368,7 @@
       'isotope': None,
       'isotope_name': InvalidElementError,
       'mass': MissingAtomicDataError,
+      'mass_energy': MissingAtomicDataError,
       'integer_charge': 0,
       'mass_number': InvalidIsotopeError,
       'element_name': InvalidElementError,
@@ -383,6 +394,8 @@
 
     (Particle('C'), {},
      {'particle': 'C',
+      'atomic_number': 6,
+      'element': 'C',
       }),
 
     (Particle('C'), {'Z': 3, 'mass_numb': 14},
diff --git a/plasmapy/atomic/tests/test_special_particles.py b/plasmapy/atomic/tests/test_special_particles.py
index 6588e28c..c2d8ca07 100644
--- a/plasmapy/atomic/tests/test_special_particles.py
+++ b/plasmapy/atomic/tests/test_special_particles.py
@@ -1,6 +1,6 @@
 import pytest
 
-from ..special_particles import (ParticleZoo, _Particles)
+from plasmapy.atomic.special_particles import (ParticleZoo, _Particles)
 
 particle_antiparticle_pairs = [
     ('e-', 'e+'),