Repository: PlasmaPy/PlasmaPy
Pull request #122 (2017-09-26 17:59:46)

Reorganize and refactor atomic subpackage
This pull request makes the following changes to the `atomic` subpackage:
- Rename `element_symbol` to `atomic_symbol`
- Rename `energy_from_nuclear_reaction` to `nuclear_reaction_energy`
- Move `nuclear_reaction_energy` and `nuclear_binding_energy` to new file called `nuclear.py`
- Clean up and reorganize docstrings in `atomic.py`
- Improve variable name clarity and consistency in `atomic.py`
- Change `atomic_symbols_list` to a dictionary called `atomic_symbols` in `elements.py`
- Use `warnings.warn` when there is a `UserWarning` instead of `raise`
- Disallow neutrons in `atomic_symbol` and `element_name`, but continue to allow them in `isotope_symbol`, `isotope_mass`, and `mass_number` because neutrons do count as nuclides
- Add helper functions to identify atomic special cases, including:
  - `__is_hydrogen`
  - `__is_positron`
  - `__is_electron`
  - `__is_antiproton`
  - `__is_neutron`
  - `__is_alpha`


Patch:
diff --git a/.gitignore b/.gitignore
index 4915c15b..016c3d65 100644
--- a/.gitignore
+++ b/.gitignore
@@ -104,4 +104,10 @@ ENV/
 .#*
 
 # Auxilliary files associated with vim
-*.swp
\ No newline at end of file
+*.swp
+
+# Miscellaneous files
+.idea/
+Untitled*
+untitled*
+*debuglog*
\ No newline at end of file
diff --git a/plasmapy/atomic/__init__.py b/plasmapy/atomic/__init__.py
index 60daa17b..31b1b675 100644
--- a/plasmapy/atomic/__init__.py
+++ b/plasmapy/atomic/__init__.py
@@ -1,11 +1,12 @@
 """Physical and mathematical constants for use within PlasmaPy"""
-from .atomic import (element_symbol, isotope_symbol, atomic_number,
+from .atomic import (atomic_symbol, isotope_symbol, atomic_number,
                      mass_number, element_name, standard_atomic_weight,
-                     isotope_mass, ion_mass, nuclear_binding_energy, half_life,
-                     energy_from_nuclear_reaction, is_isotope_stable,
+                     isotope_mass, ion_mass, half_life, is_isotope_stable,
                      known_isotopes, common_isotopes, stable_isotopes,
                      isotopic_abundance, charge_state)
 
+from .nuclear import (nuclear_binding_energy, nuclear_reaction_energy)
+
 from .elements import Elements
 
 from .isotopes import Isotopes
diff --git a/plasmapy/atomic/atomic.py b/plasmapy/atomic/atomic.py
index d888daec..5ea6f182 100644
--- a/plasmapy/atomic/atomic.py
+++ b/plasmapy/atomic/atomic.py
@@ -3,23 +3,24 @@
 import numpy as np
 import re
 from astropy import units as u, constants as const
-from .elements import atomic_symbols_list, atomic_symbols_dict, Elements
+from warnings import warn
+from .elements import atomic_symbols, atomic_symbols_dict, Elements
 from .isotopes import Isotopes
 
 
-# The code contained within element_symbol(), isotope_symbol(), and
-# __extract_charge_state() is designed to catch all of the special
+# The code contained within atomic_symbol(), isotope_symbol(), and
+# _extract_charge_state() is designed to catch all of the special
 # cases for different inputs.  Complexity is concentrated in these
 # functions so that the rest of the functions can be simpler.
 
-def element_symbol(argument):
+def atomic_symbol(argument):
     """Returns the atomic symbol.
 
     Parameters
     ----------
     argument: string or integer
-        A string representing an element, isotope, or ion or an integer
-        representing an atomic number
+        A string representing an element, isotope, or ion or an
+        integer representing an atomic number
 
     Returns
     -------
@@ -33,7 +34,8 @@ def element_symbol(argument):
 
     ValueError:
         If the argument cannot be used to identify the element, the
-        isotope has not been discovered, or the charge state is invalid.
+        isotope has not been discovered, or the charge state is
+        invalid.
 
     See also
     --------
@@ -43,101 +45,98 @@ def element_symbol(argument):
 
     Notes
     -----
-    This function returns the symbol of the element rather than the symbol of
-    an isotope.  For example, 'deuterium', 'T', or 'hydrogen-2' will yield 'H';
-    'alpha' will yield 'He'; and 'iron-56' or 'Fe-56' will yield 'Fe'.
-
-    This function is case insensitive when there is no ambiguity associated
-    with case.  However, this function will return 'H' for hydrogen for lower
-    case 'p' but capital 'P' if the argument is 'P' for phosphorus.  This
-    function will also return lower case 'n' if the argument is lower case 'n'
-    for neutrons, but capital 'N' for nitrogen if the argument is capital 'N'.
+    This function returns the symbol of the element rather than the
+    symbol of an isotope.  For example, 'deuterium', 'T', or
+    'hydrogen-2' will yield 'H'; 'alpha' will yield 'He'; and
+    'iron-56' or 'Fe-56' will yield 'Fe'.
+
+    This function is case insensitive when there is no ambiguity
+    associated with case.  However, this function will return 'H' for
+    hydrogen for lower case 'p' but capital 'P' if the argument is 'P'
+    for phosphorus.  This function will return 'N' for nitrogen if the
+    argument is capital 'N', but will not accept lower case 'n' for
+    neutrons.
 
     Examples
     --------
-    >>> element_symbol('helium')
+    >>> atomic_symbol('helium')
     'He'
-    >>> element_symbol(42)
+    >>> atomic_symbol(42)
     'Mo'
-    >>> element_symbol('D')
+    >>> atomic_symbol('D')
     'H'
-    >>> element_symbol('C-13')
+    >>> atomic_symbol('C-13')
     'C'
-    >>> element_symbol('alpha')
+    >>> atomic_symbol('alpha')
     'He'
-    >>> element_symbol('79')
+    >>> atomic_symbol('79')
     'Au'
-    >>> element_symbol('N'), element_symbol('n')  # Nitrogen, neutron
-    ('N', 'n')
-    >>> element_symbol('P'), element_symbol('p')  # Phosphorus, proton
-    ('P', 'p')
+    >>> atomic_symbol('N')  # Nitrogen
+    'N'
+    >>> atomic_symbol('P'), atomic_symbol('p')  # Phosphorus, proton
+    ('P', 'H')
 
     """
 
-    argument, charge_state = __extract_charge_state(argument)
+    if _is_neutron(argument):
+        raise ValueError("Neutrons do not have an atomic symbol")
+
+    argument, Z = _extract_charge_state(argument)
 
     if not isinstance(argument, (str, int)):
-        raise TypeError("The first argument in element_symbol must be either "
+        raise TypeError("The first argument in atomic_symbol must be either "
                         "a string representing an element or isotope, or an "
                         "integer representing the atomic number (or 0 for "
-                        " neutrons).")
+                        "neutrons).")
 
     if isinstance(argument, str) and argument.isdigit():
         argument = int(argument)
 
     if isinstance(argument, int):
 
-        if 0 <= argument <= 118:
-            symbol = atomic_symbols_list[argument]
-        else:
-            raise ValueError(str(argument)+" is an invalid atomic number in "
-                             "element_symbol")
+        try:
+            element = atomic_symbols[argument]
+        except Exception:
+            raise ValueError(f"{argument} is an invalid atomic number in "
+                             "atomic_symbol")
 
+    elif _is_hydrogen(argument):
+        element = 'H'
+    elif _is_alpha(argument):
+        element = 'He'
     elif isinstance(argument, str):
 
-        if argument in ['n', 'neutron', 'n-1']:
-            return 'n'
-        elif argument in ['p', 'p+'] or argument.lower() in \
-                ['d', 't', 'proton', 'protium', 'deuterium', 'deuteron',
-                 'triton', 'tritium']:
-            return 'H'
-        elif argument.lower() == 'alpha':
-            return 'He'
-
         if argument.count('-') == 1:
             dash_position = argument.find('-')
             mass_numb = argument[dash_position+1:]
             if not mass_numb.isdigit():
-                raise ValueError("Invalid isotope format in element_symbol")
+                raise ValueError("Invalid isotope format in atomic_symbol")
             argument = argument[:dash_position]
         else:
             mass_numb = ''
 
-        if atomic_symbols_dict.keys().__contains__(argument.lower()):
-            symbol = atomic_symbols_dict[argument.lower()]
-        elif atomic_symbols_list.__contains__(argument.capitalize()):
-            symbol = argument.capitalize()
+        if argument.lower() in atomic_symbols_dict.keys():
+            element = atomic_symbols_dict[argument.lower()]
+        elif argument in atomic_symbols.values():
+            element = argument.capitalize()
         else:
-            raise ValueError(argument+" is an invalid argument for "
-                             "element_symbol")
+            raise ValueError(f"{argument} is an invalid argument for "
+                             "atomic_symbol")
 
         if mass_numb.isdigit():
-            isotope = symbol.capitalize() + '-' + mass_numb
-            if isotope == 'H-2':
-                isotope = 'D'
-            if isotope == 'H-3':
-                isotope = 'T'
+
+            isotope = element.capitalize() + '-' + mass_numb
 
             if isotope not in Isotopes.keys():
-                raise ValueError("The input in element_symbol corresponding "
-                                 "to " + isotope + " is not a valid isotope.")
+                raise ValueError("The input in atomic_symbol corresponding "
+                                 f"to {isotope} is not a valid isotope.")
 
-    if charge_state is not None and \
-            charge_state > Elements[symbol]['atomic_number']:
+    if Z is not None and \
+            Z > Elements[element]['atomic_number']:
         raise ValueError("Cannot have an ionization state greater than the "
-                         "atomic number in element_name.")
+                         "atomic number.")
 
-    return symbol
+    return element
 
 
 def isotope_symbol(argument, mass_numb=None):
@@ -146,42 +145,44 @@ def isotope_symbol(argument, mass_numb=None):
     Parameters
     ----------
     argument: integer or string
-        A string representing an element, isotope, or ion or an integer
-        representing an atomic number
+        A string representing an element, isotope, or ion or an
+        integer representing an atomic number
 
     mass_numb: integer or string
-        An integer or string representing the mass number of the isotope.
+        An integer or string representing the mass number of the
+        isotope.
 
     Returns
     -------
     symbol: string
-        The isotopic symbol. The result will generally be returned as something
-        like 'He-4' or 'Au-197', but will return 'D' for deuterium and 'T' for
-        tritium.
+        The isotopic symbol. The result will generally be returned as
+        something like 'He-4' or 'Au-197', but will return 'D' for
+        deuterium and 'T' for tritium.
 
     Raises
     ------
     ValueError:
-        If insufficient or contradictory isotope information is provided, the
-        element cannot be determined from the first argument, or the mass
-        number exceeds the atomic number.
+        If insufficient or contradictory isotope information is
+        provided, the element cannot be determined from the first
+        argument, or the mass number exceeds the atomic number.
 
     TypeError:
-        If isotope information cannot be found because one or both inputs is
-        of an inappropriate type.
+        If isotope information cannot be found because one or both
+        inputs is of an inappropriate type.
 
     UserWarning:
         If redundant isotope information is provided.
 
     See also
     --------
-    element_symbol : returns atomic symbol instead of isotopic symbol
+    atomic_symbol : returns atomic symbol instead of isotopic symbol
 
     Notes
     -----
-    This function returns the symbol of the element rather than the symbol of
-    an isotope.  For example, 'deuterium', 'T', or 'hydrogen-2' will yield 'H';
-    'alpha' will yield 'He'; and 'iron-56' or 'Fe-56' will yield 'Fe'.
+    This function returns the symbol of the element rather than the
+    symbol of an isotope.  For example, 'deuterium', 'T', or
+    'hydrogen-2' will yield 'H'; 'alpha' will yield 'He'; and
+    'iron-56' or 'Fe-56' will yield 'Fe'.
 
     Examples
     --------
@@ -205,7 +206,7 @@ def isotope_symbol(argument, mass_numb=None):
         return argument
 
     if isinstance(argument, str):
-        argument, charge_state = __extract_charge_state(argument)
+        argument, charge_state = _extract_charge_state(argument)
 
     if isinstance(argument, str) and argument.isdigit():
         argument = int(argument)
@@ -222,7 +223,8 @@ def isotope_symbol(argument, mass_numb=None):
                         "integer representing the atomic number (or 0 for "
                         " neutrons).")
 
-    if not (isinstance(mass_numb, int) or mass_numb is None):
+    if not (isinstance(mass_numb, int) or
+            mass_numb is None):  # coveralls: ignore
         raise TypeError("The second argument in isotope_symbol must be an "
                         "integer (or a string containing an integer) that "
                         "represents the mass number of an isotope.")
@@ -231,12 +233,14 @@ def isotope_symbol(argument, mass_numb=None):
         raise ValueError("Insufficient information to determine element and "
                          "mass number in isotope_symbol.")
 
+    if _is_neutron(argument, mass_numb):
+        return 'n'
+
     try:
-        element = element_symbol(argument)
+        element = atomic_symbol(argument)
     except Exception:
-        raise ValueError("The first argument of isotope_symbol (" +
-                         str(argument) + ") does not correspond to a valid "
-                         "element or isotope.")
+        raise ValueError(f"The first argument of isotope_symbol ({argument})"
+                         "does not correspond to a valid element or isotope.")
 
     # Get mass number from argument, check for redundancies, and take
     # care of special cases.
@@ -246,8 +250,6 @@ def isotope_symbol(argument, mass_numb=None):
             dash_position = argument.find('-')
             mass_numb_from_arg = argument[dash_position+1:].strip()
             mass_numb_from_arg = int(mass_numb_from_arg)
-        elif argument == 'n' or argument.lower() == 'neutron':
-            mass_numb_from_arg = 1
         elif argument in ['p', 'p+'] or \
                 argument.lower() in ['protium', 'proton']:
             mass_numb_from_arg = 1
@@ -255,7 +257,7 @@ def isotope_symbol(argument, mass_numb=None):
             mass_numb_from_arg = 2
         elif argument.lower() in ['t', 'tritium', 'triton']:
             mass_numb_from_arg = 3
-        elif argument.lower() in ['alpha']:
+        elif _is_alpha(argument):
             mass_numb_from_arg = 4
         else:
             mass_numb_from_arg = None
@@ -266,21 +268,18 @@ def isotope_symbol(argument, mass_numb=None):
 
         if mass_numb is not None and mass_numb_from_arg is not None:
             if mass_numb == mass_numb_from_arg:
-                raise UserWarning("Redundant mass number information in " +
-                                  "isotope_symbol from inputs: (" +
-                                  str(argument)+", " + str(mass_numb) + ")")
-            else:
+                warn("Redundant mass number information in isotope_symbol "
+                     f"from inputs: {argument}, {mass_numb}", UserWarning)
+            else:  # coveralls: ignore
                 raise ValueError("Contradictory mass number information in "
                                  "isotope_symbol.")
 
         if mass_numb_from_arg is not None:
             mass_numb = mass_numb_from_arg
 
-    isotope = element + '-' + str(mass_numb)
+    isotope = f"{element}-{mass_numb}"
 
-    if isotope == 'n-1':
-        isotope = 'n'
-    elif isotope == 'H-2':
+    if isotope == 'H-2':
         isotope = 'D'
     elif isotope == 'H-3':
         isotope = 'T'
@@ -293,8 +292,7 @@ def isotope_symbol(argument, mass_numb=None):
 
 
 def atomic_number(argument):
-    """Returns the atomic number (the number of protons in an atom)
-    from an atomic symbol or name.
+    """Returns the number of protons in an atom, isotope, or ion.
 
     Parameters
     ----------
@@ -304,12 +302,13 @@ def atomic_number(argument):
     Returns
     -------
     atomic_number: integer
-        An integer representing the atomic number of the element or isotope.
+        An integer representing the atomic number of the element or
+        isotope.
 
     See also
     --------
-    mass_number : returns the mass number (the total number of protons and
-        neutrons) of an isotope.
+    mass_number : returns the mass number (the total number of protons
+        and neutrons) of an isotope.
 
     Examples
     --------
@@ -324,8 +323,14 @@ def atomic_number(argument):
 
     """
 
-    atomic_symbol = element_symbol(argument)
-    atomic_numb = Elements[atomic_symbol]['atomic_number']
+    try:
+        element = atomic_symbol(argument)
+        atomic_numb = Elements[element]['atomic_number']
+    except ValueError:
+        raise ValueError("Unable to identify element in atomic_number")
+    except TypeError:
+        raise TypeError("Invalid type in atomic_number")
+
     return atomic_numb
 
 
@@ -362,21 +367,22 @@ def is_isotope_stable(argument, mass_numb=None):
 
     try:
         isotope = isotope_symbol(argument, mass_numb)
-    except (UserWarning, ValueError):
-        raise ValueError("Unable to determine isotope in input for "
-                         "is_isotope_stable")
+    except ValueError:
+        raise ValueError("Invalid isotope in is_isotope_stable")
+    except TypeError:
+        raise TypeError("Invalid input to is_isotope_stable")
 
     try:
         is_stable = Isotopes[isotope]['is_stable']
-    except Exception:
+    except Exception:  # coveralls: ignore
         ValueError("No data on stability of " + isotope)
 
     return is_stable
 
 
 def half_life(argument, mass_numb=None):
-    """Returns the half-life in seconds for unstable isotopes, and numpy.inf
-    for stable isotopes.
+    """Returns the half-life in seconds for unstable isotopes, and
+    numpy.inf for stable isotopes.
 
     Parameters
     ----------
@@ -422,8 +428,7 @@ def half_life(argument, mass_numb=None):
         isotope = isotope_symbol(argument, mass_numb)
     except ValueError:
         raise ValueError("Cannot determine isotope information from these " +
-                         "inputs to half_life: (" + str(argument) + ", " +
-                         str(mass_numb) + ")")
+                         f"inputs to half_life: {argument}, {mass_numb}")
     except TypeError:
         raise TypeError("Incorrect argument type for half_life")
 
@@ -434,24 +439,25 @@ def half_life(argument, mass_numb=None):
             half_life_sec = Isotopes[isotope]['half_life']
     except Exception:
         half_life_sec = None
-        raise UserWarning("The half-life for isotope " + isotope +
-                          " is not available; returning None.")
+        warn(f"The half-life for isotope {isotope} is not available; "
+             "returning None.", UserWarning)
 
     return half_life_sec
 
 
 def mass_number(isotope):
-    """Get the mass number (the number of protons and neutrals) of an isotope.
+    """Get the mass number (the number of protons and neutrons) of an
+    isotope.
 
     Parameters
     ----------
     isotope : string
-        A string representing an isotope.
+        A string representing an isotope or a neutron.
 
     Returns
     -------
     mass_number : integer
-       An integer representing the mass number of the isotope.
+       The total number of protons plus neutrons in a nuclide.
 
     Raises
     ------
@@ -463,7 +469,8 @@ def mass_number(isotope):
 
     See also
     --------
-    atomic_number : returns the number of protons in an isotope or element
+    atomic_number : returns the number of protons in an isotope or
+        element
 
     Examples
     --------
@@ -483,13 +490,13 @@ def mass_number(isotope):
     """
 
     try:
-        symbol = isotope_symbol(isotope)
-        mass_numb = Isotopes[symbol]["mass_number"]
+        isotope = isotope_symbol(isotope)
+        mass_numb = Isotopes[isotope]["mass_number"]
     except TypeError:
         raise("Incorrect type for mass_number input.")
     except ValueError:
-        raise ValueError("Mass number not able to be found from input " +
-                         str(isotope))
+        raise ValueError("Mass number not able to be found from input "
+                         f"{isotope}")
 
     return mass_numb
 
@@ -500,8 +507,8 @@ def element_name(argument):
     Parameters
     ----------
     argument : string or integer
-        A string representing an element, isotope, or ion or an integer
-        representing an atomic number
+        A string representing an element, isotope, or ion or an
+        integer representing an atomic number
 
     Returns
     -------
@@ -510,7 +517,7 @@ def element_name(argument):
 
     See also
     --------
-    element_symbol : returns the atomic symbol
+    atomic_symbol : returns the atomic symbol
 
     isotope_symbol : returns the symbol of an isotope
 
@@ -530,14 +537,21 @@ def element_name(argument):
 
     """
 
-    atomic_symbol = element_symbol(argument)
-    name = Elements[atomic_symbol]["name"]
+    try:
+        element = atomic_symbol(argument)
+        name = Elements[element]["name"]
+    except ValueError:
+        raise ValueError("Unable to identify element in element_name")
+    except TypeError:
+        raise TypeError("Invalid input to element_name")
+
     return name
 
 
 def standard_atomic_weight(argument):
-    """Returns the standard (conventional) atomic weight of an element based on
-    the relative abundances of isotopes in terrestrial environments.
+    """Returns the standard (conventional) atomic weight of an element
+    based on the relative abundances of isotopes in terrestrial
+    environments.
 
     Parameters
     ----------
@@ -547,37 +561,41 @@ def standard_atomic_weight(argument):
 
     Returns
     -------
-    atomic_weight: astropy.units.Quantity with units of astropy.units.u
-        The standard atomic weight of an element based on values frmo
+
+    atomic_weight: astropy.units.Quantity with units of u
+        The standard atomic weight of an element based on values from
+        NIST
 
     Raises
     ------
     ValueError:
-        If the argument cannot be used to identify an element; the argument
-        represents an isotope, ion, or neutron; or no standard atomic weight
-        is provided for an element.
+        If the argument cannot be used to identify an element; the
+        argument represents an isotope, ion, or neutron; or no
+        standard atomic weight is provided for an element.
 
     See also
     --------
     isotope_mass : returns the atomic mass of an isotope.
 
-    ion_mass : returns the mass of an ion of an element or isotope, accounting
-        for reduction of mass from the neutral state due to different numbers
-        of electrons.
+    ion_mass : returns the mass of an ion of an element or isotope,
+        accounting for reduction of mass from the neutral state due to
+        different numbers of electrons.
 
     Notes
     -----
-    Standard atomic weight data are most readily available for the terrestrial
-    environment, so this function may not be wholly appropriate for space and
-    astrophysical environments.
-
-    The relative abundances of different isotopes of an element sometimes vary
-    naturally in different locations within the terrestrial environment.  The
-    CIAAW provides ranges for these element, which include H, Li, B, C, N, O,
-    Mg, Si, S, Cl, Br, Tl.  This function provides a single value from the
-    CIAWW 2015 standard values when a single value is given, and the lower
-    accuracy conventional value given by Meija et al. (2013,
-    doi:10.1515/pac-2015-0305) for the elements where a range is given.
+    Standard atomic weight data are most readily available for the
+    terrestrial environment, so this function may not be wholly
+    appropriate for space and astrophysical environments.
+
+    The relative abundances of different isotopes of an element
+    sometimes vary naturally in different locations within the
+    terrestrial environment.  The CIAAW provides ranges for these
+    element, which include H, Li, B, C, N, O, Mg, Si, S, Cl, Br, Tl.
+    This function provides a single value from the CIAWW 2015 standard
+    values when a single value is given, and the lower accuracy
+    conventional value given by Meija et al. (2013,
+    doi:10.1515/pac-2015-0305) for the elements where a range is
+    given.
 
     Examples
     --------
@@ -598,7 +616,7 @@ def standard_atomic_weight(argument):
 
     """
 
-    argument, charge_state = __extract_charge_state(argument)
+    argument, charge_state = _extract_charge_state(argument)
 
     if charge_state is not None and charge_state != 0:
         raise ValueError("Use ion_mass to get masses of charged particles.")
@@ -608,20 +626,20 @@ def standard_atomic_weight(argument):
     except Exception:
         isotope = ''
 
-    if isotope == 'n':
+    if _is_neutron(isotope):
         raise ValueError("Use isotope_mass('n') or plasmapy.constants.m_n "
                          "instead of standard_atomic_weight to get neutron "
                          "mass")
     elif '-' in isotope or isotope in ['D', 'T']:
         raise ValueError("Use isotope_mass to get masses of isotopes")
 
-    atomic_symbol = element_symbol(argument)
+    element = atomic_symbol(argument)
 
     try:
-        atomic_weight = Elements[atomic_symbol]['atomic_mass']
+        atomic_weight = Elements[element]['atomic_mass']
     except Exception:
         raise ValueError("No standard atomic weight is available for " +
-                         atomic_symbol)
+                         element)
 
     return atomic_weight
 
@@ -632,8 +650,8 @@ def isotope_mass(argument, mass_numb=None):
     Parameters
     ----------
     argument : string or integer
-        A string representing an element, isotope, or ion or an integer
-        representing an atomic number
+        A string representing an element, isotope, or ion or an
+        integer representing an atomic number
 
     mass_numb : integer (optional)
         The mass number of the isotope.
@@ -650,11 +668,11 @@ def isotope_mass(argument, mass_numb=None):
 
     See also
     --------
-    standard_atomic_weight : returns atomic weight of an element based on
-        terrestrial abundances of isotopes
+    standard_atomic_weight : returns atomic weight of an element based
+        on terrestrial abundances of isotopes
 
-    ion_mass : returns the mass of an ion of an element or isotope, accounting
-        for loss of electrons
+    ion_mass : returns the mass of an ion of an element or isotope,
+        accounting for loss of electrons
 
     Notes
     -----
@@ -673,14 +691,19 @@ def isotope_mass(argument, mass_numb=None):
 
     """
 
-    argument, charge_state = __extract_charge_state(argument)
+    argument, charge_state = _extract_charge_state(argument)
 
     if charge_state is not None and charge_state != 0:
         raise ValueError("Use ion_mass instead of isotope_mass for masses of "
                          "charged particles")
 
-    symbol = isotope_symbol(argument, mass_numb)
-    atomic_mass = Isotopes[symbol]['atomic_mass']
+    try:
+        isotope = isotope_symbol(argument, mass_numb)
+        atomic_mass = Isotopes[isotope]['atomic_mass']
+    except ValueError:
+        raise ValueError("Unable to identify isotope in isotope_mass")
+    except TypeError:
+        raise TypeError("Invalid input to isotope_mass")
 
     return atomic_mass
 
@@ -699,7 +722,8 @@ def ion_mass(argument, Z=None, mass_numb=None):
         within the mass range that is appropriate for an ion.
 
     Z: integer (optional)
-        The ionization state of the ion (defaulting to a charge of Z=1)
+        The ionization state of the ion (defaulting to a charge of
+        Z=1)
 
     mass_numb: integer (optional)
         The mass number of an isotope.
@@ -707,7 +731,8 @@ def ion_mass(argument, Z=None, mass_numb=None):
     Returns
     -------
     m_i: Quantity
-        The mass of a single ion of the isotope or element with charge state Z.
+        The mass of a single ion of the isotope or element with charge
+        state Z.
 
     Raises
     ------
@@ -715,45 +740,47 @@ def ion_mass(argument, Z=None, mass_numb=None):
         The argument is not a string, integer, or Quantity.
 
     ValueError
-        If the argument represents a particle other than an ion, the ionization
-        state exceeds the atomic number, or no isotope mass or standard atomic
-        weight is available.
+        If the argument represents a particle other than an ion, the
+        ionization state exceeds the atomic number, or no isotope mass
+        or standard atomic weight is available.
 
     UserWarning
-        If a mass was inputted and it is outside of the range of known isotopes
-        or electrons/positrons.
+        If a mass was inputted and it is outside of the range of known
+        isotopes or electrons/positrons.
 
     UnitConversionError
         If the argument is a Quantity but does not have units of mass.
 
     See also
     --------
-    standard_atomic_mass : returns the conventional atomic mass of an element
-        based on terrestrial values and assuming the atom is neutral.
+    standard_atomic_mass : returns the conventional atomic mass of an
+        element based on terrestrial values and assuming the atom is
+        neutral.
 
-    isotope_mass : returns the mass of an isotope (if available) assuming the
-        atom is neutral.
+    isotope_mass : returns the mass of an isotope (if available)
+        assuming the atom is neutral.
 
     Notes
     -----
-    This function in general finds the mass of an isotope (or the standard
-    atomic weight based on terrestrial values if a unique isotope cannot be
-    identified), and then substracts the mass of Z electrons.  If Z is not
-    provided as an input, then this function assumes that the ion is singly
-    ionized.
+    This function in general finds the mass of an isotope (or the
+    standard atomic weight based on terrestrial values if a unique
+    isotope cannot be identified), and then substracts the mass of Z
+    electrons.  If Z is not provided as an input, then this function
+    assumes that the ion is singly ionized.
 
     Specific values are returns for protons, deuterons, tritons, alpha
     particles, and positrons.
 
-    Calling ion_mass('H') does not return the mass of a proton but instea
-    uses hydrogen's standard atomic weight based on terrestrial values.  To
-    get the mass of a proton, use ion_mass('p').
+    Calling ion_mass('H') does not return the mass of a proton but
+    instead uses hydrogen's standard atomic weight based on
+    terrestrial values.  To get the mass of a proton, use
+    ion_mass('p').
 
-    This function can accept a Quantity in units of mass.  If the Quantity
-    is close to the mass of an electron or positron, it will return the
-    mass of an electron or positron to full known precision.  If the
-    Quantity is within the mass range of known isotopes, it will return the
-    mass that is inputted to it.
+    This function can accept a Quantity in units of mass.  If the
+    Quantity is close to the mass of an electron or positron, it will
+    return the mass of an electron or positron to full known
+    precision.  If the Quantity is within the mass range of known
+    isotopes, it will return the mass that is inputted to it.
 
     Examples
     --------
@@ -782,11 +809,6 @@ def ion_mass(argument, Z=None, mass_numb=None):
 
     """
 
-    # TODO: Replace ion_mass() with particle_mass()
-
-    # TODO: Find a better way to account for the special cases that
-    # are complicating this and other atomic functions
-
     if isinstance(argument, u.Quantity) and Z is None and mass_numb is None:
 
         try:
@@ -800,23 +822,26 @@ def ion_mass(argument, Z=None, mass_numb=None):
         elif 1.66e-27 <= m_i.value < 7e-25:  # mass range of known isotopes
             return m_i
         else:
-            raise UserWarning("The mass that was inputted to ion_mass and is "
-                              "being returned from ion_mass is outside of the "
-                              "range of known isotopes or electrons/ions.")
+            warn("The mass that was inputted to ion_mass and is being returned"
+                 " from ion_mass is outside of the range of known isotopes or "
+                 "electrons/ions.", UserWarning)
+            return m_i
 
     if isinstance(argument, str) and \
             str(argument).lower() in ['e+', 'positron', 'e', 'e-', 'electron']:
         return const.m_e
 
-    if argument in ['p', 'p+', 'p-'] or str(argument).lower() in \
-            ['proton', 'protium', 'antiproton'] and Z is None:
+    if argument in ['p', 'p+'] or str(argument).lower() in \
+            ['proton', 'protium'] and Z is None:
+        return const.m_p
+    elif _is_antiproton(argument) and Z is None:
         return const.m_p
 
-    if atomic_number(argument) == 0:
+    if _is_neutron(argument, mass_numb):
         raise ValueError("Use isotope_mass or m_n to get mass of neutron")
 
     if isinstance(argument, str):
-        argument, Z_from_arg = __extract_charge_state(argument)
+        argument, Z_from_arg = _extract_charge_state(argument)
     else:
         Z_from_arg = None
 
@@ -851,226 +876,59 @@ def ion_mass(argument, Z=None, mass_numb=None):
                          "atomic number in ion_mass")
 
     try:
-        isotope_symb = isotope_symbol(argument, mass_numb)
-        if isotope_symb == 'D' and Z == 1:
-            return 3.343583719e-27 * u.kg
-        elif isotope_symb == 'T' and Z == 1:
-            return 5.007356665e-27 * u.kg
-        atomic_mass = isotope_mass(isotope_symb)
+        isotope = isotope_symbol(argument, mass_numb)
     except Exception:
-        try:
-            atomic_mass = standard_atomic_weight(argument)
-        except Exception:
-            errormessage = "No isotope mass or standard atomic weight is " +\
-                "available to get ion mass for " + str(argument)
-            if isinstance(mass_numb, int):
-                errormessage += " with mass number " + str(mass_numb)
-
-            raise ValueError(errormessage)
-
-    m_i = (atomic_mass - Z*const.m_e).to(u.kg)
-
-    return m_i
-
-
-def nuclear_binding_energy(argument, mass_numb=None):
-    """Returns the nuclear binding energy associated with an isotope.
-
-    Parameters
-    ----------
-    argument: string or integer
-        A string representing an element or isotope, or an integer representing
-        the atomic number of an element.
-
-    mass_numb: integer, 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
-    -------
-    binding_energy: Quantity
-        The binding energy of the nucleus in units of Joules.
-
-    See also
-    --------
-    energy_from_nuclear_reaction : Returns the change in binding energy during
-        nuclear fusion or fission reactions.
-
-    Examples
-    --------
-    >>> nuclear_binding_energy('Fe-56')
-    <Quantity 7.674002331034521e-11 J>
-    >>> nuclear_binding_energy(26, 56)
-    <Quantity 7.674002331034521e-11 J>
-    >>> nuclear_binding_energy('p')  # proton
-    <Quantity 0.0 J>
-
-    >>> from astropy import units as u
-    >>> before = nuclear_binding_energy("D") + nuclear_binding_energy("T")
-    >>> after = nuclear_binding_energy("alpha")
-    >>> (after - before).to(u.MeV)  # released energy from D + T --> alpha + n
-    <Quantity 17.589296207151556 MeV>
-
-    """
-
-    if argument == 'n' and mass_numb is None or mass_numb == 1:
-        return 0.0 * u.J
-
-    isotopic_symbol = isotope_symbol(argument, mass_numb)
-    isotopic_mass = isotope_mass(isotopic_symbol)
-    number_of_protons = atomic_number(argument)
-
-    if mass_numb is None:
-        mass_numb = mass_number(argument)
-    number_of_neutrons = mass_numb - number_of_protons
-
-    if number_of_protons == 1 and number_of_neutrons == 0:
-        binding_energy = 0.0 * u.J
+        is_isotope = False
     else:
-        mass_of_nucleons = (number_of_protons * const.m_p +
-                            number_of_neutrons * const.m_n)
-        mass_defect = mass_of_nucleons - isotopic_mass
-        binding_energy = mass_defect * const.c**2
+        is_isotope = True
 
-    return binding_energy.to(u.J)
-
-
-def energy_from_nuclear_reaction(reaction):
-    """Returns the released energy from a nuclear fusion reaction.
-
-    Parameters
-    ----------
-    reaction: string
-        A string representing the reaction, like "D + T --> alpha + n"
-
-    Returns
-    -------
-    energy: the change in nuclear binding energy, which will be positive
-    if the reaction releases and negative if the reaction is energetically
-    unfavorable.
-
-    Raises
-    ------
-    ValueError:
-        If the input is not a valid reaction, there is insufficient
-        information to determine an isotope, or if the number of nucleons
-        is not conserved during the reaction.
-
-    TypeError:
-        If the input is not a string.
+    if is_isotope:
 
-    See also
-    --------
-    nuclear_binding_energy : finds the binding energy of an isotope
-
-    Examples
-    --------
-    >>> energy_from_nuclear_reaction("D + T --> alpha + n")
-    <Quantity 17.589296207151556 MeV>
-    >>> triple_alpha1 = 'alpha + He-4 --> Be-8'
-    >>> triple_alpha2 = 'Be-8 + alpha --> carbon-12'
-    >>> energy_triplealpha1 = energy_from_nuclear_reaction(triple_alpha1)
-    >>> energy_triplealpha2 = energy_from_nuclear_reaction(triple_alpha2)
-    >>> print(energy_triplealpha1, '\n', energy_triplealpha2)
-    -0.09183948324626812 MeV
-    7.366586766240317 MeV
-    >>> energy_triplealpha1.to(u.J)
-    <Quantity -1.471430677988809e-14 J>
-    >>> energy_triplealpha2.cgs
-    <Quantity -1.4714306779888094e-07 erg>
-    >>> energy_from_nuclear_reaction('alpha + alpha --> 2alpha')
-    <Quantity 0.0 MeV>
-
-    """
+        if isotope == 'D' and Z == 1:
+            return 3.343583719e-27 * u.kg
+        elif isotope == 'T' and Z == 1:
+            return 5.007356665e-27 * u.kg
 
-    import re
+        atomic_mass = isotope_mass(isotope)
 
-    def _get_isotopes_list(side):
-        """Parse a side of a reaction to get a list of the isotopes."""
-        pre_list = re.split(' \+ ', side)
-        isotopes_list = []
-        for item in pre_list:
-            item = item.strip()
-            try:
-                if item == 'n':
-                    symbol = 'n'
-                else:
-                    symbol = isotope_symbol(item)
-                isotopes_list.append(symbol)
-            except Exception:
-                try:
-                    multiplier_string = ''
-                    while item[0].isdigit():
-                        multiplier_string += item[0]
-                        item = item[1:]
-                    symbol = isotope_symbol(item)
-                    for i in range(0, int(multiplier_string)):
-                        isotopes_list.append(symbol)
-                except Exception:
-                    raise
-        return isotopes_list
-
-    def _mass_number_of_list(isotopes_list):
-        """Find the total number of nucleons in a list of isotopes."""
-        mass_numb = 0
-        for symbol in isotopes_list:
-            mass_numb += mass_number(symbol)
-        return mass_numb
-
-    def _add_binding_energies(isotopes_list):
-        """Finds the total binding energy from a list of isotopes."""
-        total_binding_energy = 0.0*u.MeV
-        for isotope in isotopes_list:
-            total_binding_energy += nuclear_binding_energy(isotope)
-        return total_binding_energy
-
-    if not isinstance(reaction, str):
-        raise TypeError("The input of energy_from_nuclear_reaction "
-                        "must be a string representing the reaction (e.g., "
-                        "'D + T --> He + n')")
+    else:
 
-    try:
-        LHS, RHS = re.split('-+>', reaction)
-    except Exception:
-        raise ValueError("The left and right hand sides of the reaction "
-                         "should be separated by '-->'")
+        try:
+            atomic_mass = standard_atomic_weight(argument)
+        except Exception:  # coveralls: ignore
 
-    reactants = _get_isotopes_list(LHS)
-    products = _get_isotopes_list(RHS)
+            errormessage = ("No isotope mass or standard atomic weight is "
+                            f"available to get ion mass for {argument}")
 
-    mass_num_reactants = _mass_number_of_list(reactants)
-    mass_num_products = _mass_number_of_list(products)
+            if isinstance(mass_numb, int):
+                errormessage += f" with mass number {mass_numb}"
 
-    if mass_num_reactants != mass_num_products:
-        raise ValueError("Mass numbers on LHS and RHS do not match for "
-                         "reaction "+reaction)
+            raise ValueError(errormessage)
 
-    binding_energy_before = _add_binding_energies(reactants)
-    binding_energy_after = _add_binding_energies(products)
-    energy = binding_energy_after - binding_energy_before
+    m_i = (atomic_mass - Z * const.m_e).to(u.kg)
 
-    return energy.to(u.MeV)
+    return m_i
 
 
 def known_isotopes(argument=None):
-    """Returns a list of all known isotopes of an element, or a list of all
-    known isotopes of every element if no input is provided.
+    """Returns a list of all known isotopes of an element, or a list
+    of all known isotopes of every element if no input is provided.
 
     Parameters
     ----------
     argument: integer or string, optional
-        A string representing an element, isotope, or ion or an integer
-        representing an atomic number
+        A string representing an element, isotope, or ion or an
+        integer representing an atomic number
 
     Returns
     -------
     isotopes_list: list of strings or empty list
-        List of all of the isotopes of an element that have been discovered,
-        sorted from lowest mass number to highest mass number.  If no argument
-        is provided, then a list of all known isotopes of every element will
-        be returned that is sorted by atomic number, with entries for each
-        element sorted by mass number.
+        List of all of the isotopes of an element that have been
+        discovered, sorted from lowest mass number to highest mass
+        number.  If no argument is provided, then a list of all known
+        isotopes of every element will be returned that is sorted by
+        atomic number, with entries for each element sorted by mass
+        number.
 
     Raises
     ------
@@ -1083,10 +941,11 @@ def known_isotopes(argument=None):
 
     See also
     --------
-    common_isotopes : returns isotopes with non-zero isotopic abundances
+    common_isotopes : returns isotopes with non-zero isotopic
+        abundances
 
-    stable_isotopes : returns isotopes that are stable against radioactive
-        decay
+    stable_isotopes : returns isotopes that are stable against
+        radioactive decay
 
     Examples
     --------
@@ -1102,7 +961,7 @@ def known_isotopes(argument=None):
     """
 
     def known_isotopes_for_element(argument):
-        element = element_symbol(argument)
+        element = atomic_symbol(argument)
         isotopes = []
         for isotope in Isotopes.keys():
             if element + '-' in isotope and isotope[0:len(element)] == element:
@@ -1117,13 +976,11 @@ def known_isotopes_for_element(argument):
 
     if argument is not None:
         try:
-            element = element_symbol(argument)
-            if element == 'n':
-                raise
+            element = atomic_symbol(argument)
             isotopes_list = known_isotopes_for_element(element)
         except Exception:
             raise ValueError("known_isotopes is unable to get isotopes from "
-                             "an input of: " + str(argument))
+                             f"an input of: {argument}")
     elif argument is None:
         isotopes_list = []
         for atomic_numb in range(1, 119):
@@ -1133,15 +990,15 @@ def known_isotopes_for_element(argument):
 
 
 def common_isotopes(argument=None, most_common_only=False):
-    """Returns a list of isotopes of an element with an isotopic abundances
-    greater than zero, or if no input is provided, a list of all such isotopes
-    for every element.
+    """Returns a list of isotopes of an element with an isotopic
+    abundances greater than zero, or if no input is provided, a list
+    of all such isotopes for every element.
 
     Parameters
     ----------
     argument: integer or string, optional
-        A string or integer representing an atomic number or element, or a
-        string represnting an isotope.
+        A string or integer representing an atomic number or element,
+        or a string represnting an isotope.
 
     most_common_only: boolean
         If set to True, return only the most common isotope
@@ -1149,12 +1006,13 @@ def common_isotopes(argument=None, most_common_only=False):
     Returns
     -------
     isotopes_list: list of strings or empty list
-        List of all isotopes of an element with isotopic abundances greater
-        than zero, sorted from most abundant to least abundant.  If no isotopes
-        have isotopic abundances greater than zero, this function will return
-        an empty list.  If no arguments are provided, then a list of all common
-        isotopes of all elements will be provided that is sorted by atomic
-        number, with entries for each element sorted from most abundant to
+        List of all isotopes of an element with isotopic abundances
+        greater than zero, sorted from most abundant to least
+        abundant.  If no isotopes have isotopic abundances greater
+        than zero, this function will return an empty list.  If no
+        arguments are provided, then a list of all common isotopes of
+        all elements will be provided that is sorted by atomic number,
+        with entries for each element sorted from most abundant to
         least abundant.
 
     Raises
@@ -1164,17 +1022,19 @@ def common_isotopes(argument=None, most_common_only=False):
 
     Notes
     -----
-    The isotopic abundances are based on the terrestrial environment and may
-    not be wholly appropriate for space and astrophysical applications.
+    The isotopic abundances are based on the terrestrial environment
+    and may not be wholly appropriate for space and astrophysical
+    applications.
 
     See also
     --------
-    known_isotopes : returns a list of isotopes that have been discovered
+    known_isotopes : returns a list of isotopes that have been
+        discovered
 
-    stable_isotopes : returns isotopes that are stable against radioactive
-        decay
+    stable_isotopes : returns isotopes that are stable against
+        radioactive decay
 
-    isotopic_abundance : returns the relat
+    isotopic_abundance : returns the relative isotopic abundance
 
     Examples
     --------
@@ -1210,15 +1070,15 @@ def common_isotopes_for_element(argument, most_common_only):
         return sorted_isotopes
 
     if argument is not None:
+
         try:
-            element = element_symbol(argument)
-            if element == 'n':
-                raise
+            element = atomic_symbol(argument)
             isotopes_list = \
                 common_isotopes_for_element(element, most_common_only)
         except Exception:
             raise ValueError("common_isotopes is unable to get isotopes from "
-                             "an input of: " + str(argument))
+                             f"an input of: {argument}")
+
     elif argument is None:
         isotopes_list = []
         for atomic_numb in range(1, 119):
@@ -1228,40 +1088,43 @@ def common_isotopes_for_element(argument, most_common_only):
     return isotopes_list
 
 
-def stable_isotopes(argument=None, unstable_instead=False):
-    """Returns a list of all stable isotopes of an element, or if no input is
-    provided, a list of all such isotopes for every element.
+def stable_isotopes(argument=None, unstable=False):
+    """Returns a list of all stable isotopes of an element, or if no
+    input is provided, a list of all such isotopes for every element.
 
     Parameters
     ----------
     argument: integer or string
-        A string or integer representing an atomic number or element, or a
-        string represnting an isotope.
+        A string or integer representing an atomic number or element,
+        or a string represnting an isotope.
 
-    unstable_instead: boolean
-        If set to True, this function will return a list of the unstable
-        isotopes instead of the stable isotopes.
+    unstable: boolean
+        If set to True, this function will return a list of the
+        unstable isotopes instead of the stable isotopes.
 
     Returns
     -------
     StableIsotopes: list of strings or empty list
-        List of all stable isotopes of an element, sorted from lowest mass
-        number.  If an element has no stable isotopes, this function returns
-        an empty list.
+        List of all stable isotopes of an element, sorted from lowest
+        mass number.  If an element has no stable isotopes, this
+        function returns an empty list.
 
     Notes
     -----
-    There are 254 isotopes for which no radioactive decay has been observed.
-    It is possible that some isotopes will be discovered to be unstable but
-    with extremely long half-lives.  For example, bismuth-209 was recently
-    discovered to have a half-life of about 1.9e19 years.  However, such
-    isotopes can be regarded as virtually stable for most applications.
+    There are 254 isotopes for which no radioactive decay has been
+    observed.  It is possible that some isotopes will be discovered to
+    be unstable but with extremely long half-lives.  For example,
+    bismuth-209 was recently discovered to have a half-life of about
+    1.9e19 years.  However, such isotopes can be regarded as virtually
+    stable for most applications.
 
     See also
     --------
-    known_isotopes : returns a list of isotopes that have been discovered
+    known_isotopes : returns a list of isotopes that have been
+        discovered
 
-    common_isotopes : returns isotopes with non-zero isotopic abundances
+    common_isotopes : returns isotopes with non-zero isotopic
+        abundances
 
     Examples
     --------
@@ -1278,7 +1141,7 @@ def stable_isotopes(argument=None, unstable_instead=False):
 
     Find unstable isotopes
 
-    >>>
+    >>> stable_isotopes('U', unstable_instead=True)
 
     """
 
@@ -1290,19 +1153,17 @@ def stable_isotopes_for_element(argument, stable_only):
 
     if argument is not None:
         try:
-            element = element_symbol(argument)
-            if element == 'n':
-                raise
+            element = atomic_symbol(argument)
             isotopes_list = \
-                stable_isotopes_for_element(element, not unstable_instead)
+                stable_isotopes_for_element(element, not unstable)
         except Exception:
             raise ValueError("stable_isotopes is unable to get isotopes from "
-                             "an input of: " + str(argument))
+                             f"an input of: {argument}")
     elif argument is None:
         isotopes_list = []
         for atomic_numb in range(1, 119):
             isotopes_list += \
-                stable_isotopes_for_element(atomic_numb, not unstable_instead)
+                stable_isotopes_for_element(atomic_numb, not unstable)
 
     return isotopes_list
 
@@ -1313,12 +1174,12 @@ def isotopic_abundance(argument, mass_numb=None):
     Parameters
     ----------
     argument: string or integer
-        A string representing an element or isotope, or an integer representing
-        the atomic number of an element.
+        A string representing an element or isotope, or an integer
+        representing the atomic number of an element.
 
     mass_numb: integer
-        The mass number of an isotope, which is required if and only if the
-        first argument can only be used
+        The mass number of an isotope, which is required if and only
+        if the first argument can only be used
 
     Returns
     -------
@@ -1332,12 +1193,12 @@ def isotopic_abundance(argument, mass_numb=None):
 
     Notes
     -----
-    Isotopic composition data are most readily available for the terrestrial
-    environment, so this function may not be wholly appropriate for space
-    and astrophysical applications.
+    Isotopic composition data are most readily available for the
+    terrestrial environment, so this function may not be wholly
+    appropriate for space and astrophysical applications.
 
-    The data retrieved from this routine are those recommended by NIST as of
-    2017.
+    The data retrieved from this routine are those recommended by NIST
+    as of 2017.
 
     Examples
     --------
@@ -1372,8 +1233,8 @@ def charge_state(argument):
     Parameters
     ----------
     argument : string
-        String representing an element or isotope followed by charge state
-        information.
+        String representing an element or isotope followed by charge
+        state information.
 
     Returns
     -------
@@ -1383,15 +1244,17 @@ def charge_state(argument):
     Raises
     ------
     ValueError:
-        If the charge state or isotope information is invalid, or the charge
-        state exceeds the atomic number.
+        If the charge state or isotope information is invalid, or the
+        charge state exceeds the atomic number.
 
     UserWarning:
-        If the input represents an ion with a charge state that is below -3.
+        If the input represents an ion with a charge state that is
+        below -3.
 
     Notes
     -----
-    This function supports two formats for the charge state information.
+    This function supports two formats for the charge state
+    information.
 
     The first format is a string that has information for the element
     or isotope at the beginning, a space in between, and the charge
@@ -1416,13 +1279,12 @@ def charge_state(argument):
 
     """
 
-    if argument in ['e', 'e-', 'p-'] or \
-            argument.lower() in ['electron', 'antiproton']:
+    if _is_electron(argument):
         return -1
-    elif argument == 'e+' or argument.lower() == 'positron':
+    elif _is_positron(argument):
         return 1
 
-    argument, Z = __extract_charge_state(argument)
+    argument, Z = _extract_charge_state(argument)
 
     try:
         atomic_numb = atomic_number(argument)
@@ -1435,14 +1297,69 @@ def charge_state(argument):
                          "number.")
 
     if Z is not None and (Z < -atomic_numb-1 or Z < -3):
-        raise UserWarning("Element " + element_symbol(argument) + " has a "
-                          "charge of " + str(Z) + " which is "
-                          "unlikely to occur in nature.")
+        warn(f"Element {atomic_symbol(argument)} has a charge of {Z}"
+             " which is unlikely to occur in nature.", UserWarning)
 
     return Z
 
 
-def __extract_charge_state(argument):
+def electric_charge(argument):
+    """Returns the electric charge (in coulombs) of an ion or other
+    particle
+
+    Parameters
+    ----------
+    argument : string
+        String representing an element or isotope followed by charge
+        state information.
+
+    Returns
+    -------
+    charge: Quantity
+        The electric charge in coulombs.
+
+    Raises
+    ------
+    ValueError:
+        If the charge state or isotope information is invalid, or the
+        charge state exceeds the atomic number.
+
+    UserWarning:
+        If the input represents an ion with a charge state that is
+        below -3.
+
+    Notes
+    -----
+    This function supports two formats for the charge state
+    information.
+
+    The first format is a string that has information for the element
+    or isotope at the beginning, a space in between, and the charge
+    state information in the form of an integer followed by a plus or
+    minus sign, or a plus or minus sign followed by an integer.
+
+    The second format is a string containing element information at
+    the beginning, following by one or more plus or minus signs.
+
+    This function returns -1.6021766208e-19 C for electrons and
+    1.6021766208e-19 C for positrons.
+
+    Examples
+    --------
+    >>> q = electric_charge('p')
+    <Quantity 1.6021766208e-19 C>
+
+    """
+
+    try:
+        charge = charge_state(argument) * const.e.to('C')
+    except Exception:
+        raise ValueError("Invalid input to electric_charge")
+
+    return charge
+
+
+def _extract_charge_state(argument):
     """Splits strings containing element or isotope and charge state
     information into a string without the charge state information and
     the charge state as an integer (or None if no charge state
@@ -1451,8 +1368,8 @@ def __extract_charge_state(argument):
     Parameters
     ----------
     argument : string
-        String containing information for an element or isotope in any of
-        the allowed formats, followed by charge state information.
+        String containing information for an element or isotope in any
+        of the allowed formats, followed by charge state information.
 
     Returns
     -------
@@ -1460,8 +1377,9 @@ def __extract_charge_state(argument):
         The original string with charge state information removed.
 
     Z : integer
-        The charge state of an ion (e.g., this will return 1 if one electron
-        has been removed and -1 if one electron has been gained)
+        The charge state of an ion (e.g., this will return 1 if one
+        electron has been removed and -1 if one electron has been
+        gained)
 
     Notes
     -----
@@ -1472,12 +1390,12 @@ def __extract_charge_state(argument):
 
     Examples
     --------
-    >>> isotope, Z = __extract_charge_state('Fe-56+++')
+    >>> isotope, Z = _extract_charge_state('Fe-56+++')
     >>> print(isotope)
     'Fe-56'
     >>> print(Z)
     3
-    >>> __extract_charge_state('D +1')
+    >>> _extract_charge_state('D +1')
     ('D', 1)
 
     """
@@ -1520,78 +1438,150 @@ def __extract_charge_state(argument):
 
         if not (check1 and check2 and check3):
             raise ValueError("The following input does not have valid charge "
-                             "state information: " + argument + ion_info)
+                             f"state information: {argument}{ion_info}")
 
     elif argument.endswith(('-', '+')):  # For cases like 'Fe++' or 'Si-'
 
         char = argument[-1]
         match = re.match(r"["+char+"]*", argument[::-1])
+
         charge_state = match.span()[1]
 
         if char == '-':
             charge_state = -charge_state
 
-        if argument.count(char) != np.abs(charge_state):
-            raise ValueError("The following input does not have valid charge "
-                             "state information: " + argument)
-
         argument = argument[0:len(argument)-match.span()[1]]
 
+        if argument.endswith(('-', '+')):
+            raise ValueError("Invalid charge state information")
+
     else:
         charge_state = None
 
     if charge_state is not None and charge_state < -3:
-        raise UserWarning("Element " + element_symbol(argument) + " has a "
-                          "charge of " + str(charge_state) + " which is "
-                          "unlikely to occur in nature.")
+        warn(f"Element {atomic_symbol(argument)} has a charge of "
+             f"{charge_state} which is unlikely to occur in nature.",
+             UserWarning)
 
     return argument, charge_state
 
 
-def electric_charge(argument):
-    """Returns the electric charge (in coulombs) of an ion or other particle
+def _is_neutron(argument, mass_numb=None):
+    """Returns True if the argument corresponds to a neutron, and
+    False otherwise."""
 
-    Parameters
-    ----------
-    argument : string
-        String representing an element or isotope followed by charge state
-        information.
+    if argument == 0 and mass_numb == 1:
+        return True
+    elif isinstance(argument, str) and mass_numb is None:
+        if argument in ('n', 'n-1') or argument.lower() in ['neutron', 'n0']:
+            return True
+        else:
+            return False
+    else:
+        return False
 
-    Returns
-    -------
-    charge: Quantity
-        The electric charge in coulombs.
 
-    Raises
-    ------
-    ValueError:
-        If the charge state or isotope information is invalid, or the charge
-        state exceeds the atomic number.
+def _is_hydrogen(argument, can_be_atomic_number=False):
+    """Returns True if the argument corresponds to hydrogen, and False
+    otherwise."""
 
-    UserWarning:
-        If the input represents an ion with a charge state that is below -3.
+    if argument == 'p-':
+        return False
 
-    Notes
-    -----
-    This function supports two formats for the charge state information.
+    case_sensitive_aliases = ['p', 'p+', 'H', 'D', 'T']
 
-    The first format is a string that has information for the element
-    or isotope at the beginning, a space in between, and the charge
-    state information in the form of an integer followed by a plus or
-    minus sign, or a plus or minus sign followed by an integer.
+    case_insensitive_aliases = ['proton', 'protium', 'deuterium',
+                                'deuteron', 'triton', 'tritium', 'hydrogen']
 
-    The second format is a string containing element information at
-    the beginning, following by one or more plus or minus signs.
+    for mass_numb in range(1, 8):
+        case_sensitive_aliases.append(f'H-{mass_numb}')
+        case_insensitive_aliases.append(f'hydrogen-{mass_numb}')
 
-    This function returns -1.6021766208e-19 C for electrons and
-    1.6021766208e-19 C for positrons.
+    if isinstance(argument, str):
 
-    Examples
-    --------
-    >>> q = electric_charge('p')
-    <Quantity 1.6021766208e-19 C>
-    """
+        argument, Z = _extract_charge_state(argument)
 
-    charge = charge_state(argument) * const.e.to('C')
+        if argument in case_sensitive_aliases:
+            is_hydrogen = True
+        elif argument.lower() in case_insensitive_aliases:
+            is_hydrogen = True
+        else:
+            is_hydrogen = False
 
-    return charge
+        if is_hydrogen and Z is not None and Z > 1:
+            raise ValueError("Invalid charge state of hydrogen")
+
+    elif argument == 1 and can_be_atomic_number:
+        is_hydrogen = True
+    else:
+        is_hydrogen = False
+
+    return is_hydrogen
+
+
+def _is_electron(argument):
+    """Returns True if the argument corresponds to an electron, and False
+    otherwise."""
+
+    if not isinstance(argument, str):
+        return False
+
+    if argument in ['e', 'e-'] or argument.lower() == 'electron':
+        return True
+    else:
+        return False
+
+
+def _is_positron(argument):
+    """Returns True if the argument corresponds to a positron, and False
+    otherwise."""
+
+    if not isinstance(argument, str):
+        return False
+
+    if argument == 'e+' or argument.lower() == 'positron':
+        return True
+    else:
+        return False
+
+
+def _is_antiproton(argument):
+    """Returns True if the argument corresponds to an antiproton, and
+    False otherwise."""
+
+    if not isinstance(argument, str):
+        return False
+
+    if argument == 'p-' or argument.lower() == 'antiproton':
+        return True
+    else:
+        return False
+
+
+def _is_alpha(argument):
+    """Returns True if the argument corresponds to an alpha particle,
+    and False otherwise."""
+
+    if not isinstance(argument, str):
+        return False
+
+    if argument.lower() == 'alpha':
+        is_alpha = True
+    else:
+        argument, Z = _extract_charge_state(argument)
+
+        if Z != 2:
+            is_alpha = False
+        elif argument[-2:] != '-4':
+            is_alpha = False
+        else:
+
+            dash_position = argument.find('-')
+            argument = argument[:dash_position]
+
+            if argument.lower() == 'helium' or argument == 'He':
+                is_alpha = True
+            else:
+                is_alpha = False
+
+    return is_alpha
diff --git a/plasmapy/atomic/elements.py b/plasmapy/atomic/elements.py
index 5efa06a4..5add30ca 100644
--- a/plasmapy/atomic/elements.py
+++ b/plasmapy/atomic/elements.py
@@ -2,21 +2,28 @@
 
 from astropy import units as u, constants as const
 
-atomic_symbols_list = ["n", "H", "He", "Li", "Be", "B", "C", "N", "O",
-                       "F", "Ne", "Na", "Mg", "Al", "Si", "P", "S",
-                       "Cl", "Ar", "K", "Ca", "Sc", "Ti", "V", "Cr",
-                       "Mn", "Fe", "Co", "Ni", "Cu", "Zn", "Ga", "Ge",
-                       "As", "Se", "Br", "Kr", "Rb", "Sr", "Y", "Zr",
-                       "Nb", "Mo", "Tc", "Ru", "Rh", "Pd", "Ag", "Cd",
-                       "In", "Sn", "Sb", "Te", "I", "Xe", "Cs", "Ba",
-                       "La", "Ce", "Pr", "Nd", "Pm", "Sm", "Eu", "Gd",
-                       "Tb", "Dy", "Ho", "Er", "Tm", "Yb", "Lu", "Hf",
-                       "Ta", "W", "Re", "Os", "Ir", "Pt", "Au", "Hg",
-                       "Tl", "Pb", "Bi", "Po", "At", "Rn", "Fr", "Ra",
-                       "Ac", "Th", "Pa", "U", "Np", "Pu", "Am", "Cm",
-                       "Bk", "Cf", "Es", "Fm", "Md", "No", "Lr", "Rf",
-                       "Db", "Sg", "Bh", "Hs", "Mt", "Ds", "Rg", "Cn",
-                       "Nh", "Fl", "Mc", "Lv", "Ts", "Og"]
+atomic_symbols = {1: "H", 2: "He", 3: "Li", 4: "Be", 5: "B", 6: "C", 7: "N",
+                  8: "O", 9: "F", 10: "Ne", 11: "Na", 12: "Mg", 13: "Al",
+                  14: "Si", 15: "P", 16: "S", 17: "Cl", 18: "Ar", 19: "K",
+                  20: "Ca", 21: "Sc", 22: "Ti", 23: "V", 24: "Cr", 25: "Mn",
+                  26: "Fe", 27: "Co", 28: "Ni", 29: "Cu", 30: "Zn", 31: "Ga",
+                  32: "Ge", 33: "As", 34: "Se", 35: "Br", 36: "Kr", 37: "Rb",
+                  38: "Sr", 39: "Y", 40: "Zr", 41: "Nb", 42: "Mo", 43: "Tc",
+                  44: "Ru", 45: "Rh", 46: "Pd", 47: "Ag", 48: "Cd", 49: "In",
+                  50: "Sn", 51: "Sb", 52: "Te", 53: "I", 54: "Xe", 55: "Cs",
+                  56: "Ba", 57: "La", 58: "Ce", 59: "Pr", 60: "Nd", 61: "Pm",
+                  62: "Sm", 63: "Eu", 64: "Gd", 65: "Tb", 66: "Dy", 67: "Ho",
+                  68: "Er", 69: "Tm", 70: "Yb", 71: "Lu", 72: "Hf", 73: "Ta",
+                  74: "W", 75: "Re", 76: "Os", 77: "Ir", 78: "Pt", 79: "Au",
+                  80: "Hg", 81: "Tl", 82: "Pb", 83: "Bi", 84: "Po", 85: "At",
+                  86: "Rn", 87: "Fr", 88: "Ra", 89: "Ac", 90: "Th", 91: "Pa",
+                  92: "U", 93: "Np", 94: "Pu", 95: "Am", 96: "Cm", 97: "Bk",
+                  98: "Cf", 99: "Es", 100: "Fm", 101: "Md", 102: "No",
+                  103: "Lr", 104: "Rf", 105: "Db", 106: "Sg", 107: "Bh",
+                  108: "Hs", 109: "Mt", 110: "Ds", 111: "Rg", 112: "Cn",
+                  113: "Nh", 114: "Fl", 115: "Mc", 116: "Lv", 117: "Ts",
+                  118: "Og"}
+
 
 atomic_symbols_dict = {"hydrogen": "H", "helium": "He", "lithium": "Li",
                        "beryllium": "Be", "boron": "B", "carbon": "C",
@@ -60,11 +67,9 @@
                        "copernicium": "Cn", "nihonium": "Nh",
                        "flerovium": "Fl", "moscovium": "Mc",
                        "livermorium": "Lv", "tennessine": "Ts",
-                       "oganesson": "Og", "neutron": "n"}
+                       "oganesson": "Og"}
 
 Elements = {
-    "n": {"atomic_number": 0, "symbol": "n",
-          "name": "neutron"},
     "H": {"atomic_number": 1, "symbol": "H",
           "atomic_mass": 1.008*u.u, "name": "hydrogen"},
     "He": {"atomic_number": 2, "symbol": "He",
diff --git a/plasmapy/atomic/nuclear.py b/plasmapy/atomic/nuclear.py
new file mode 100644
index 00000000..d22d0523
--- /dev/null
+++ b/plasmapy/atomic/nuclear.py
@@ -0,0 +1,183 @@
+"""Functions that are related to nuclear reactions."""
+
+from astropy import units, constants
+import re
+from .atomic import (isotope_symbol, mass_number, isotope_mass, atomic_number,
+                     _is_neutron)
+
+
+def nuclear_binding_energy(argument, mass_numb=None):
+    """Returns the nuclear binding energy associated with an isotope.
+
+    Parameters
+    ----------
+    argument: string or integer
+        A string representing an element or isotope, or an integer
+        representing the atomic number of an element.
+
+    mass_numb: integer, 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
+    -------
+    binding_energy: Quantity
+        The binding energy of the nucleus in units of Joules.
+
+    See also
+    --------
+    nuclear_reaction_energy : Returns the change in binding energy
+        during nuclear fusion or fission reactions.
+
+    Examples
+    --------
+    >>> nuclear_binding_energy('Fe-56')
+    <Quantity 7.674002331034521e-11 J>
+    >>> nuclear_binding_energy(26, 56)
+    <Quantity 7.674002331034521e-11 J>
+    >>> nuclear_binding_energy('p')  # proton
+    <Quantity 0.0 J>
+
+    >>> from astropy import units as u
+    >>> before = nuclear_binding_energy("D") + nuclear_binding_energy("T")
+    >>> after = nuclear_binding_energy("alpha")
+    >>> (after - before).to(u.MeV)  # released energy from D + T --> alpha + n
+    <Quantity 17.589296207151556 MeV>
+
+    """
+
+    if _is_neutron(argument) and mass_numb is None or mass_numb == 1:
+        return 0.0 * units.J
+
+    isotopic_symbol = isotope_symbol(argument, mass_numb)
+
+    isotopic_mass = isotope_mass(isotopic_symbol)
+    number_of_protons = atomic_number(argument)
+
+    if mass_numb is None:
+        mass_numb = mass_number(argument)
+    number_of_neutrons = mass_numb - number_of_protons
+
+    if number_of_protons == 1 and number_of_neutrons == 0:
+        binding_energy = 0.0 * units.J
+    else:
+        mass_of_nucleons = (number_of_protons * constants.m_p +
+                            number_of_neutrons * constants.m_n)
+        mass_defect = mass_of_nucleons - isotopic_mass
+        binding_energy = mass_defect * constants.c**2
+
+    return binding_energy.to(units.J)
+
+
+def nuclear_reaction_energy(reaction):
+    """Returns the released energy from a nuclear fusion reaction.
+
+    Parameters
+    ----------
+    reaction: string
+        A string representing the reaction, like "D + T --> alpha + n"
+
+    Returns
+    -------
+    energy: Quantity
+        The change in nuclear binding energy, which will be positive
+        if the reaction releases energy and negative if the reaction
+        is energetically unfavorable.
+
+    Raises
+    ------
+    ValueError:
+        If the input is not a valid reaction, there is insufficient
+        information to determine an isotope, or if the number of
+        nucleons is not conserved during the reaction.
+
+    TypeError:
+        If the input is not a string.
+
+    See also
+    --------
+    nuclear_binding_energy : finds the binding energy of an isotope
+
+    Examples
+    --------
+    >>> from astropy import units as u
+    >>> nuclear_reaction_energy("D + T --> alpha + n")
+    <Quantity 17.589296207151556 MeV>
+    >>> triple_alpha1 = 'alpha + 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, '\n', energy_triplealpha2)
+    -0.09183948324626812 MeV
+    7.366586766240317 MeV
+    >>> energy_triplealpha1.to(u.J)
+    <Quantity -1.471430677988809e-14 J>
+    >>> energy_triplealpha2.cgs
+    <Quantity -1.4714306779888094e-07 erg>
+    >>> nuclear_reaction_energy('alpha + alpha --> 2alpha')
+    <Quantity 0.0 MeV>
+
+    """
+
+    def _get_isotopes_list(side):
+        """Parse a side of a reaction to get a list of the isotopes."""
+        pre_list = re.split(' \+ ', side)
+        isotopes_list = []
+        for item in pre_list:
+            item = item.strip()
+            try:
+                isotope = isotope_symbol(item)
+                isotopes_list.append(isotope)
+            except Exception:
+                try:
+                    multiplier_string = ''
+                    while item[0].isdigit():
+                        multiplier_string += item[0]
+                        item = item[1:]
+                    isotope = isotope_symbol(item)
+                    for i in range(0, int(multiplier_string)):
+                        isotopes_list.append(isotope)
+                except Exception:
+                    raise
+        return isotopes_list
+
+    def _mass_number_of_list(isotopes_list):
+        """Find the total number of nucleons in a list of isotopes."""
+        mass_numb = 0
+        for isotope in isotopes_list:
+            mass_numb += mass_number(isotope)
+        return mass_numb
+
+    def _add_binding_energies(isotopes_list):
+        """Finds the total binding energy from a list of isotopes."""
+        total_binding_energy = 0.0*units.MeV
+        for isotope in isotopes_list:
+            total_binding_energy += nuclear_binding_energy(isotope)
+        return total_binding_energy
+
+    if not isinstance(reaction, str):
+        raise TypeError("The input of nuclear_reaction_energy must be a string"
+                        " representing a reaction (e.g., 'D + T --> He + n')")
+
+    try:
+        LHS, RHS = re.split('-+>', reaction)
+    except Exception:
+        raise ValueError("The left and right hand sides of the reaction "
+                         "should be separated by '-->'")
+
+    reactants = _get_isotopes_list(LHS)
+    products = _get_isotopes_list(RHS)
+
+    mass_num_reactants = _mass_number_of_list(reactants)
+    mass_num_products = _mass_number_of_list(products)
+
+    if mass_num_reactants != mass_num_products:
+        raise ValueError("Mass numbers on LHS and RHS do not match for "
+                         "reaction " + reaction)
+
+    binding_energy_before = _add_binding_energies(reactants)
+    binding_energy_after = _add_binding_energies(products)
+    energy = binding_energy_after - binding_energy_before
+
+    return energy.to(units.MeV)
diff --git a/plasmapy/classes/plasma.py b/plasmapy/classes/plasma.py
index d1cc8e98..44ccf7b9 100644
--- a/plasmapy/classes/plasma.py
+++ b/plasmapy/classes/plasma.py
@@ -14,38 +14,44 @@
 class Plasma():
     """Core class for describing and calculating plasma parameters.
 
-
-
     Attributes
     ----------
     x : `astropy.units.Quantity`
-        x-coordinates within the plasma domain. Equal to the `domain_x` input parameter.
+        x-coordinates within the plasma domain. Equal to the
+        `domain_x` input parameter.
     y : `astropy.units.Quantity`
-        y-coordinates within the plasma domain. Equal to the `domain_y` input parameter.
+        y-coordinates within the plasma domain. Equal to the
+        `domain_y` input parameter.
     z : `astropy.units.Quantity`
-        z-coordinates within the plasma domain. Equal to the `domain_z` input parameter.
+        z-coordinates within the plasma domain. Equal to the
+        `domain_z` input parameter.
     grid : `astropy.units.Quantity`
-        (3, x, y, z) array containing the values of each coordinate at every point in the domain.
+        (3, x, y, z) array containing the values of each coordinate at
+        every point in the domain.
     domain_shape : tuple
         Shape of the plasma domain.
     density : `astropy.units.Quantity`
         (x, y, z) array of mass density at every point in the domain.
     momentum : `astropy.units.Quantity`
-        (3, x, y, z) array of the momentum vector at every point in the domain.
+        (3, x, y, z) array of the momentum vector at every point in
+        the domain.
     pressure : `astropy.units.Quantity`
         (x, y, z) array of pressure at every point in the domain.
     magnetic_field : `astropy.units.Quantity`
-        (3, x, y, z) array of the magnetic field vector at every point in the domain.
-
+        (3, x, y, z) array of the magnetic field vector at every point
+        in the domain.
 
     Parameters
     ----------
     domain_x : `astropy.units.Quantity`
-        1D array of x-coordinates for the plasma domain. Must have units convertable to length.
+        1D array of x-coordinates for the plasma domain. Must have
+        units convertable to length.
     domain_y : `astropy.units.Quantity`
-        1D array of y-coordinates for the plasma domain. Must have units convertable to length.
+        1D array of y-coordinates for the plasma domain. Must have
+        units convertable to length.
     domain_z : `astropy.units.Quantity`
-        1D array of z-coordinates for the plasma domain. Must have units convertable to length.
+        1D array of z-coordinates for the plasma domain. Must have
+        units convertable to length.
     """
     @u.quantity_input(domain_x=u.m, domain_y=u.m, domain_z=u.m)
     def __init__(self, domain_x, domain_y, domain_z):
@@ -60,7 +66,7 @@ def __init__(self, domain_x, domain_y, domain_z):
 
         # Initiate core plasma variables
         self.density = np.zeros(self.domain_shape) * u.kg / u.m**3
-        self.momentum = np.zeros((3, *self.domain_shape)) * u.kg / (u.m**2 * u.s)
+        self.momentum = np.zeros((3, *self.domain_shape)) * u.kg / (u.m**2*u.s)
         self.pressure = np.zeros(self.domain_shape) * u.Pa
         self.magnetic_field = np.zeros((3, *self.domain_shape)) * u.T
         self.electric_field = np.zeros((3, *self.domain_shape)) * u.V / u.m


Test patch:
diff --git a/plasmapy/atomic/tests/__init__.py b/plasmapy/atomic/tests/__init__.py
index 7128d2af..b8efa29f 100644
--- a/plasmapy/atomic/tests/__init__.py
+++ b/plasmapy/atomic/tests/__init__.py
@@ -1,4 +1,4 @@
-from .test_atomic import (test_element_symbol,
+from .test_atomic import (test_atomic_symbol,
                           test_isotope_symbol,
                           test_atomic_number,
                           test_mass_number,
@@ -6,8 +6,6 @@
                           test_standard_atomic_weight,
                           test_isotope_mass,
                           test_ion_mass,
-                          test_nuclear_binding_energy,
-                          test_energy_from_nuclear_reaction,
                           test_is_isotope_stable,
                           test_isotope_calls,
                           test_half_life,
@@ -16,3 +14,6 @@
                           test_known_common_stable_isotopes,
                           test_isotopic_abundance,
                           test_charge_state)
+
+from .test_nuclear import (test_nuclear_binding_energy,
+                           test_nuclear_reaction_energy)
diff --git a/plasmapy/atomic/tests/test_atomic.py b/plasmapy/atomic/tests/test_atomic.py
index 14c62a83..c4d7e155 100644
--- a/plasmapy/atomic/tests/test_atomic.py
+++ b/plasmapy/atomic/tests/test_atomic.py
@@ -1,45 +1,46 @@
 from itertools import product
 from astropy import units as u, constants as const
 import numpy as np
-from ..atomic import (element_symbol, isotope_symbol, atomic_number,
+
+from ..atomic import (atomic_symbol, isotope_symbol, atomic_number,
                       mass_number, element_name, standard_atomic_weight,
-                      isotope_mass, ion_mass, nuclear_binding_energy,
-                      energy_from_nuclear_reaction, is_isotope_stable,
+                      isotope_mass, ion_mass, is_isotope_stable,
                       half_life, known_isotopes, common_isotopes,
                       stable_isotopes, isotopic_abundance, charge_state,
-                      electric_charge, Elements, Isotopes)
+                      electric_charge, Elements, Isotopes,
+                      _is_neutron, _is_hydrogen, _is_electron,
+                      _is_positron, _is_antiproton, _is_alpha,
+                      _extract_charge_state)
+
+from ..nuclear import (nuclear_binding_energy, nuclear_reaction_energy)
 
 import pytest
 
 # (argument, expected)
-element_symbol_table = [
+atomic_symbol_table = [
     (1, 'H'),
     ('H', 'H'),
-    ('h', 'H'),
-    ('d', 'H'),
+    ('p', 'H'),
     ('T', 'H'),
     ('deuterium', 'H'),
     ('deuteron', 'H'),
     ('Tritium', 'H'),
     ('triton', 'H'),
+    ('H-2', 'H'),
+    ('D', 'H'),
+    ('T', 'H'),
     ('H-3', 'H'),
     ('Hydrogen-3', 'H'),
     ('helium', 'He'),
     (2, 'He'),
     ('alpha', 'He'),
-    ('he', 'He'),
     ('gold', 'Au'),
     ('Gold', 'Au'),
-    ('au', 'Au'),
     (79, 'Au'),
     ('79', 'Au'),
-    ('p', 'H'),
     ('P', 'P'),
     (118, 'Og'),
-    ('neutron', 'n'),
-    ('n-1', 'n'),
     ('N-14', 'N'),
-    ('n', 'n'),
     ('N', 'N'),
     ('H +1', 'H'),
     ('H 1+', 'H'),
@@ -56,14 +57,14 @@
 
 
 @pytest.mark.parametrize(
-    'argument, expected', element_symbol_table
+    'argument, expected', atomic_symbol_table
 )
-def test_element_symbol(argument, expected):
-    assert element_symbol(argument) == expected
+def test_atomic_symbol(argument, expected):
+    assert atomic_symbol(argument) == expected
 
 
 # (argument, expected_error)
-element_symbol_error_table = [
+atomic_symbol_error_table = [
     ('H-0', ValueError),
     (3.141592653589793238462643383279502884, TypeError),
     ('Og-294b', ValueError),
@@ -72,23 +73,28 @@ def test_element_symbol(argument, expected):
     ('Fe+24', ValueError),
     ('Fe +59', ValueError),
     ('C++++++++++++++++', ValueError),
-    ('C-++++', ValueError)]
+    ('C-++++', ValueError),
+    ('neutron', ValueError),
+    ('n', ValueError),
+    ('n-1', ValueError),
+    ('h', ValueError),
+    ('d', ValueError),
+    ('he', ValueError),
+    ('au', ValueError)]
 
 
 @pytest.mark.parametrize(
-    'argument, expected_error', element_symbol_error_table
-)
-def test_element_symbol_error(argument, expected_error):
+    'argument, expected_error', atomic_symbol_error_table)
+def test_atomic_symbol_error(argument, expected_error):
     with pytest.raises(expected_error):
-        element_symbol(argument)
+        atomic_symbol(argument)
 
 
 # (argument, expected)
-isotype_symbol_table = [
+isotope_symbol_table = [
     (('He', 4), 'He-4'),
     (('helium-4',), 'He-4'),
     (('H-2',), 'D'),
-    (('d',), 'D'),
     (('Deuterium',), 'D'),
     (('deuterium',), 'D'),
     (('deuteron',), 'D'),
@@ -96,10 +102,8 @@ def test_element_symbol_error(argument, expected_error):
     (('triton',), 'T'),
     (('Hydrogen-3',), 'T'),
     (('hydrogen-3',), 'T'),
-    (('h-3',), 'T'),
     (('H-3',), 'T'),
     ((1, 2), 'D'),
-    (('h', 2), 'D'),
     (('Hydrogen', 3), 'T'),
     (('tritium',), 'T'),
     (('H', 2), 'D'),
@@ -108,35 +112,28 @@ def test_element_symbol_error(argument, expected_error):
     ((79, 197), 'Au-197'),
     (('p',), 'H-1'),
     (('beryllium-8',), 'Be-8'),
-    (('neutron',), 'n'),
-    (('n',), 'n'),
-    ((0, 1), 'n'),
-    (('n-1',), 'n'),
     (('N-13',), 'N-13'),
     (('p',), 'H-1'),
     (('proton',), 'H-1'),
     (('protium',), 'H-1'),
     (('N-13 2+',), 'N-13'),
-    (('d+',), 'D'),
-    (('Hydrogen-3 +1',), 'T')]
+    (('Hydrogen-3 +1',), 'T'),
+    (('neutron',), 'n'),
+    (('n',), 'n'),
+    ((0, 1), 'n'),
+    (('neutron',), 'n'),
+    (('Neutron',), 'n'),
+    (('n-1',), 'n')]
 
 
 @pytest.mark.parametrize(
-    "argument, expected", isotype_symbol_table)
+    "argument, expected", isotope_symbol_table)
 def test_isotope_symbol(argument, expected):
     assert isotope_symbol(*argument) == expected
 
 
 # (argument, kwargs, expected_error)
 isotope_symbol_error_table = [
-    ('H-1', {"mass_numb": 1}, UserWarning),
-    ('H-2', {"mass_numb": 2}, UserWarning),
-    ('T', {"mass_numb": 3}, UserWarning),
-    ('Li-6', {"mass_numb": 6}, UserWarning),
-    ('lithium-6', {"mass_numb": 6}, UserWarning),
-    ('alpha', {"mass_numb": 4}, UserWarning),
-    ('p', {"mass_numb": 1}, UserWarning),
-    ('n', {"mass_numb": 1}, UserWarning),
     ('Md-260', {"mass_numb": 261}, ValueError),
     ('protium', {"mass_numb": 2}, ValueError),
     ('alpha', {"mass_numb": 3}, ValueError),
@@ -151,7 +148,17 @@ def test_isotope_symbol(argument, expected):
     (4, {}, ValueError),
     ('hydrogen-444444', {}, ValueError),
     ('Fe', {"mass_numb": 2.1}, TypeError),
-    ('Fe', {"mass_numb": None}, ValueError)]
+    ('He', {"mass_numb": 'c'}, TypeError),
+    ('He-3', {"mass_numb": 4}, ValueError),
+    ('alpha', {"mass_numb": 3}, ValueError),
+    ('D', {"mass_numb": 3}, ValueError),
+    ('T', {"mass_numb": 2}, ValueError),
+    ('Fe', {"mass_numb": None}, ValueError),
+    ('d', {}, ValueError),
+    ('h-3', {}, ValueError),
+    ('h', {}, ValueError),
+    ('d+', {}, ValueError),
+]
 
 
 @pytest.mark.parametrize(
@@ -161,28 +168,40 @@ def test_isotope_symbol_error(argument, kwargs, expected_error):
         isotope_symbol(argument, **kwargs)
 
 
+# (argument, kwargs, expected_warning)
+isotope_symbol_warning_table = [
+    ('H-1', {"mass_numb": 1}, UserWarning),
+    ('H-2', {"mass_numb": 2}, UserWarning),
+    ('T', {"mass_numb": 3}, UserWarning),
+    ('Li-6', {"mass_numb": 6}, UserWarning),
+    ('lithium-6', {"mass_numb": 6}, UserWarning),
+    ('alpha', {"mass_numb": 4}, UserWarning),
+    ('p', {"mass_numb": 1}, UserWarning)]
+
+
+@pytest.mark.parametrize(
+    "argument, kwargs, expected_warning", isotope_symbol_warning_table)
+def test_isotope_symbol_error(argument, kwargs, expected_warning):
+    with pytest.warns(expected_warning):
+        isotope_symbol(argument, **kwargs)
+
+
 # (argument, expected)
 atomic_number_table = [
     ('H', 1),
-    ('d', 1),
     ('D', 1),
     ('deuterium', 1),
     ('Deuterium', 1),
-    ('t', 1),
     ('tritium', 1),
     ('p', 1),
     ('P', 15),
     ('Alpha', 2),
     ('C-12', 6),
-    ('s-36', 16),
     ('Argon', 18),
     ('protium', 1),
     ('H-3', 1),
     ('p+', 1),
     ('Be-8', 4),
-    ('n', 0),
-    ('n-1', 0),
-    ('Neutron', 0),
     ('N', 7),
     ('N 2+', 7),
     ('N +1', 7),
@@ -198,13 +217,19 @@ def test_atomic_number(argument, expected):
 atomic_number_error_table = [
     ('H-3934', ValueError),
     ('C-12b', ValueError),
-    (-1.5, TypeError)]
+    (-1.5, TypeError),
+    ('n', ValueError),
+    ('n-1', ValueError),
+    ('neutron', ValueError),
+    ('Neutron', ValueError),
+    ('d', ValueError),
+    ('t', ValueError),
+    ('s-36', ValueError)]
 
 
 @pytest.mark.parametrize(
     "argument, expected_error", atomic_number_error_table)
 def test_atomic_number_error(argument, expected_error):
-
     with pytest.raises(expected_error):
         atomic_number(argument)
 
@@ -221,6 +246,8 @@ def test_atomic_number_error(argument, expected_error):
     ('alpha', 4),
     ('p', 1),
     ('n', 1),
+    ('neutron', 1),
+    ('n-1', 1),
     ('Be-8', 8),
     ('N-13', 13),
     ('N-13 2+', 13),
@@ -238,7 +265,8 @@ def test_mass_number(isotope, expected):
     ('H-359', ValueError),
     ('C-12b', ValueError),
     (-1.5, Exception),
-    ('N-13+-+-', ValueError)]
+    ('N-13+-+-', ValueError),
+    ('h-3', ValueError)]
 
 
 @pytest.mark.parametrize(
@@ -252,17 +280,13 @@ def test_mass_number_error(argument, expected_error):
 element_name_table = [
     ('D', 'hydrogen'),
     ('deuterium', 'hydrogen'),
-    ('t', 'hydrogen'),
     ('Au', 'gold'),
-    ('pb', 'lead'),
     ('alpha', 'helium'),
     ('helium-4', 'helium'),
     ('H-2', 'hydrogen'),
-    ('d', 'hydrogen'),
     ('Deuterium', 'hydrogen'),
     ('Hydrogen-3', 'hydrogen'),
     ('hydrogen-3', 'hydrogen'),
-    ('h-3', 'hydrogen'),
     ('H-3', 'hydrogen'),
     ('tritium', 'hydrogen'),
     ('Alpha', 'helium'),
@@ -274,8 +298,6 @@ def test_mass_number_error(argument, expected_error):
     ('P', 'phosphorus'),
     ('Be-8', 'beryllium'),
     ('Li-7', 'lithium'),
-    ('n', 'neutron'),
-    (0, 'neutron'),
     ('N', 'nitrogen'),
     ('N+++', 'nitrogen'),
     ('D-', 'hydrogen')]
@@ -291,7 +313,14 @@ def test_element_name(argument, expected):
     ('vegan cupcakes', ValueError),
     ('C-13-14-15-51698024', ValueError),
     (1.24, TypeError),
-    ('H++', ValueError)]
+    ('n', ValueError),
+    ('neutron', ValueError),
+    (0, ValueError),
+    ('H++', ValueError),
+    ('t', ValueError),
+    ('pb', ValueError),
+    ('d', ValueError),
+    ('h-3', ValueError)]
 
 
 @pytest.mark.parametrize("argument, expected_error", element_name_error_table)
@@ -337,7 +366,9 @@ def test_standard_atomic_weight(argument, expected):
     ('tritium', ValueError),
     ('Au+', ValueError),
     ('Fe -2', ValueError),
-    ('Og 2+', ValueError)]
+    ('Og 2+', ValueError),
+    ('h', ValueError),
+    ('fe', ValueError)]
 
 
 @pytest.mark.parametrize("argument, expected_error",
@@ -382,6 +413,7 @@ def test_isotope_mass(arg1, arg2):
     (1.1, TypeError),
     ('alpha', ValueError),
     ('He-4 2+', ValueError),
+    ('he-4', ValueError),
     ('p', ValueError),
     ('Fe 2+', ValueError),
     ('Fe -2', ValueError),
@@ -446,10 +478,12 @@ def test_ion_mass():
     (26, {"Z": 1, "mass_numb": 'a'}, TypeError),
     (26, {"Z": 27, "mass_numb": 56}, ValueError),
     ('Og', {"Z": 1}, ValueError),
-    (1.6e-27 * u.kg, {}, UserWarning),
-    (8e-25 * u.kg, {}, UserWarning),
+    ('Og', {"mass_numb": 296, "Z": 1}, ValueError),
+    ('n', {}, ValueError),
+    ('He', {"mass_numb": 99}, ValueError),
     (1 * u.m, {}, u.UnitConversionError),
-    ('Og', {"Z": 1}, ValueError)]
+    ('Og', {"Z": 1}, ValueError),
+    ('fe-56 1+', {}, ValueError)]
 
 
 @pytest.mark.parametrize("argument, kwargs, expected_error",
@@ -459,79 +493,17 @@ def test_ion_mass_error(argument, kwargs, expected_error):
         ion_mass(argument, **kwargs)
 
 
-def test_nuclear_binding_energy():
-    assert nuclear_binding_energy('p') == 0
-    assert nuclear_binding_energy('n') == 0
-    assert nuclear_binding_energy('He-4') == nuclear_binding_energy('alpha') \
-        == nuclear_binding_energy('He', 4)
-
-
-def test_nuclear_binding_energy_D_T():
-    before = nuclear_binding_energy("D") + nuclear_binding_energy("T")
-    after = nuclear_binding_energy("alpha")
-    E_in_MeV = (after - before).to(u.MeV).value  # D + T --> alpha + n + E
-    assert np.isclose(E_in_MeV, 17.58, rtol=0.01)
-
-
-# (argument, expected_error)
-nuclear_binding_energy_table = [
-    ("H", ValueError),
-    (1.1, TypeError)]
-
-
-@pytest.mark.parametrize("argument, expected_error",
-                         nuclear_binding_energy_table)
-def test_nuclear_binding_energy_error(argument, expected_error):
-    with pytest.raises(expected_error):
-        nuclear_binding_energy(argument)
-
-
-def test_energy_from_nuclear_reaction():
-    reaction1 = 'D + T --> alpha + n'
-    reaction2 = 'T + D -> n + alpha'
-    released_energy1 = energy_from_nuclear_reaction(reaction1)
-    released_energy2 = energy_from_nuclear_reaction(reaction2)
-    assert np.isclose(released_energy1.to(u.MeV).value, 17.58, rtol=0.01)
-    assert released_energy1 == released_energy2
-
-
-def test_energy_from_nuclear_reaction_triple_alpha():
-    triple_alpha1 = 'alpha + He-4 --> Be-8'
-    triple_alpha2 = 'Be-8 + alpha --> carbon-12'
-    energy_triplealpha1 = energy_from_nuclear_reaction(triple_alpha1)
-    energy_triplealpha2 = energy_from_nuclear_reaction(triple_alpha2)
-    assert np.isclose(energy_triplealpha1.to(u.keV).value, -91.8, atol=0.1)
-    assert np.isclose(energy_triplealpha2.to(u.MeV).value, 7.367, atol=0.1)
-
-
-def test_energy_from_nuclear_reaction_alpha_decay():
-    alpha_decay_example = 'U-238 --> Th-234 + alpha'
-    energy_alpha_decay = energy_from_nuclear_reaction(alpha_decay_example)
-    assert np.isclose(energy_alpha_decay.to(u.MeV).value, 4.26975, atol=1e-5)
-
-
-def test_energy_from_nuclear_reaction_triple_alpha_r():
-    triple_alpha1_r = '4He-4 --> 2Be-8'
-    energy_triplealpha1_r = energy_from_nuclear_reaction(triple_alpha1_r)
-    assert np.isclose(energy_triplealpha1_r.to(u.keV).value,
-                      -91.8 * 2, atol=0.1)
-
-
-# (reaction, expected_error)
-energy_from_nuclear_reaction_error_table = [
-    ('H + H --> H', ValueError),
-    (1, TypeError),
-    ('H-1 + H-1 --> H-1', ValueError),
-    ("I didn't like unstable eigenfunctions "
-     "at first, but then they grew on me", ValueError)
-]
+# (argument, kwargs, expected_warning)
+ion_mass_warning_table = [
+    (1.6e-27 * u.kg, {}, UserWarning),
+    (8e-25 * u.kg, {}, UserWarning)]
 
 
-@pytest.mark.parametrize(
-    "reaction, expected_error", energy_from_nuclear_reaction_error_table)
-def test_energy_from_nuclear_reaction_error(reaction, expected_error):
-    with pytest.raises(expected_error):
-        energy_from_nuclear_reaction(reaction)
+@pytest.mark.parametrize("argument, kwargs, expected_warning",
+                         ion_mass_warning_table)
+def test_ion_mass_warnings(argument, kwargs, expected_warning):
+    with pytest.warns(expected_warning):
+        ion_mass(argument, **kwargs)
 
 
 # (argument)
@@ -549,7 +521,6 @@ def test_energy_from_nuclear_reaction_error(reaction, expected_error):
     ('Fe', 56),
     ('Fe', '56'),
     ('Fe-56',),
-    ('fe-56',),
     ('iron-56',),
     ('Iron-56',),
     (26, 56)]
@@ -573,7 +544,6 @@ def test_is_isotope_stable(argument):
     ('tritium',),
     ('neutron',),
     ('Pb-209',),
-    ('pb-209',),
     ('lead-209',),
     ('Lead-209',),
     ('Pb', 209),
@@ -590,7 +560,9 @@ def test_is_isotope_stable_false(argument):
 is_isotope_stable_error_table = [
     (('hydrogen-444444',), ValueError),
     (('hydrogen', 0), ValueError),
-    (('',), ValueError)]
+    (('',), ValueError),
+    (('pb-209',), ValueError),
+    (('h',), ValueError)]
 
 
 @pytest.mark.parametrize("argument, expected_error",
@@ -619,12 +591,12 @@ def test_half_life_unstable_isotopes():
     for isotope in Isotopes.keys():  # unstable isotopes w/o half-life data
         if 'half_life' not in Isotopes[isotope].keys() and \
                 not Isotopes[isotope].keys():
-            with pytest.raises(UserWarning):
+            with pytest.warns(UserWarning):
                 assert half_life(isotope) is None
 
 
-def test_half_lift_u_220():
-    with pytest.raises(UserWarning):
+def test_half_life_u_220():
+    with pytest.warns(UserWarning):
         half_life('U-220')
         assert half_life('U-220') is None, \
             ("If half-life data is added for this isotope, then this test "
@@ -633,11 +605,11 @@ def test_half_lift_u_220():
 
 
 atomic_TypeError_funcs_table = [
-    element_symbol, isotope_symbol, atomic_number,
+    atomic_symbol, isotope_symbol, atomic_number,
     is_isotope_stable, half_life, mass_number,
     element_name, standard_atomic_weight, isotope_mass,
     ion_mass, nuclear_binding_energy,
-    energy_from_nuclear_reaction]
+    nuclear_reaction_energy]
 atomic_TypeError_bad_arguments = [1.1, {'cats': 'bats'}, 1 + 1j]
 
 
@@ -651,7 +623,7 @@ def test_atomic_TypeErrors(function, argument):
 
 
 atomic_ValueErrors_funcs_table = [
-    element_symbol, isotope_symbol, atomic_number,
+    atomic_symbol, isotope_symbol, atomic_number,
     is_isotope_stable, half_life, mass_number,
     element_name, standard_atomic_weight]
 atomic_ValueError_bad_arguments = [-1, 119, 'grumblemuffins', 'Oj']
@@ -762,10 +734,10 @@ def test_charge_state(argument, expected):
 charge_state_error_table = [
     ('fads', ValueError),
     ('H++', ValueError),
-    ('Fe 29+', ValueError),
-    ('H---', UserWarning),
-    ('Fe -26', UserWarning),
-    ('Og 10-', UserWarning)]
+    ('h+', ValueError),
+    ('fe 1+', ValueError),
+    ('d+', ValueError),
+    ('Fe 29+', ValueError)]
 
 
 @pytest.mark.parametrize("argument, expected_error", charge_state_error_table)
@@ -774,6 +746,20 @@ def test_charge_state_error(argument, expected_error):
         charge_state(argument)
 
 
+# (argument, expected_warning)
+charge_state_warning_table = [
+    ('H---', UserWarning),
+    ('Fe -26', UserWarning),
+    ('Og 10-', UserWarning)]
+
+
+@pytest.mark.parametrize("argument, expected_warning",
+                         charge_state_warning_table)
+def test_charge_state_warnings(argument, expected_warning):
+    with pytest.warns(expected_warning):
+        charge_state(argument)
+
+
 def test_electric_charge():
     assert electric_charge('p').value == 1.6021766208e-19
     assert electric_charge('p').unit == 'C'
@@ -785,9 +771,8 @@ def test_electric_charge():
 electric_charge_error_table = [
     ('badinput', ValueError),
     (' ', ValueError),
-    ('Au 81+', ValueError),
-    ('Au 81-', UserWarning),
-    ('H---', UserWarning)]
+    ('h+', ValueError),
+    ('Au 81+', ValueError)]
 
 
 @pytest.mark.parametrize("argument, expected_error",
@@ -795,3 +780,193 @@ def test_electric_charge():
 def test_electric_charge_error(argument, expected_error):
     with pytest.raises(expected_error):
         electric_charge(argument)
+
+
+# (argument, expected_warning)
+electric_charge_warning_table = [
+    ('Au 81-', UserWarning),
+    ('H---', UserWarning)]
+
+
+@pytest.mark.parametrize("argument, expected_warning",
+                         electric_charge_warning_table)
+def test_electric_charge_error(argument, expected_warning):
+    with pytest.warns(expected_warning):
+        electric_charge(argument)
+
+
+@pytest.mark.parametrize("test_input,kwargs,expected",
+                         [('n', {}, True),
+                          ('n-1', {}, True),
+                          ('N', {}, False),
+                          ('N-1', {}, False),
+                          ('N-7', {}, False),
+                          ('neutron', {}, True),
+                          ('James Chadwick', {}, False),
+                          (0, {}, False),
+                          (0, {"mass_numb": 1}, True),
+                          ('n0', {}, True)])
+def test_is_neutron(test_input, kwargs, expected):
+    assert _is_neutron(test_input, **kwargs) == expected
+
+
+@pytest.mark.parametrize("test_input,can_be_atom_numb,expected",
+                         [('hydrogen', False, True),
+                          ('hydrogen+', False, True),
+                          ('hydrogen-', False, True),
+                          ('hydrogen--', False, True),
+                          ('H', False, True),
+                          ('H+', False, True),
+                          ('H-', False, True),
+                          ('proton', False, True),
+                          ('protium', False, True),
+                          ('deuterium', False, True),
+                          ('tritium', False, True),
+                          ('triton', False, True),
+                          ('deuteron', False, True),
+                          ('h', False, False),
+                          ('D', False, True),
+                          ('D+', False, True),
+                          ('H-2', False, True),
+                          ('H-2+', False, True),
+                          ('H-2 1+', False, True),
+                          ('H-2 +1', False, True),
+                          ('H-3 -1', False, True),
+                          ('He', False, False),
+                          ('H-1', False, True),
+                          ('H-7', False, True),
+                          ('antiproton', False, False),
+                          (1, True, True),
+                          (1, False, False),
+                          ('p-', False, False)])
+def test_is_hydrogen(test_input, can_be_atom_numb, expected):
+    assert _is_hydrogen(test_input,
+                         can_be_atomic_number=can_be_atom_numb) == expected
+
+
+@pytest.mark.parametrize("test_input,kwargs,expected_error",
+                         [('H 2+', {}, ValueError),
+                          ('D++', {}, ValueError)])
+def test_is_hydrogen_errors(test_input, kwargs, expected_error):
+    with pytest.raises(expected_error):
+        _is_hydrogen(test_input, **kwargs)
+
+
+@pytest.mark.parametrize("test_input,expected",
+                         [('e-', True),
+                          ('e+', False),
+                          ('Electron', True),
+                          ('electron', True),
+                          ('positron', False),
+                          ('p', False),
+                          ('E', False),
+                          ('E-', False),
+                          ('beta', False),
+                          (57, False)])
+def test_is_electron(test_input, expected):
+    assert _is_electron(test_input) == expected
+
+
+@pytest.mark.parametrize("test_input,expected",
+                         [('e-', False),
+                          ('e+', True),
+                          ('Electron', False),
+                          ('electron', False),
+                          ('positron', True),
+                          ('p', False),
+                          ('E', False),
+                          ('E-', False),
+                          ('beta', False),
+                          (57, False)])
+def test_is_positron(test_input, expected):
+    assert _is_positron(test_input) == expected
+
+
+@pytest.mark.parametrize("test_input,expected",
+                         [('e-', False),
+                          ('e+', False),
+                          ('Electron', False),
+                          ('electron', False),
+                          ('positron', False),
+                          ('p', False),
+                          ('E', False),
+                          ('E-', False),
+                          ('beta', False),
+                          ('p-', True),
+                          ('Antiproton', True),
+                          ('antiproton', True),
+                          ('p+', False),
+                          ('p--', False),
+                          ('P-', False),
+                          (57, False)])
+def test_is_antiproton(test_input, expected):
+    assert _is_antiproton(test_input) == expected
+
+
+@pytest.mark.parametrize("test_input,expected",
+                         [('e-', False),
+                          ('e+', False),
+                          ('Electron', False),
+                          ('electron', False),
+                          ('positron', False),
+                          ('p', False),
+                          ('E', False),
+                          ('E-', False),
+                          ('beta', False),
+                          ('p-', False),
+                          ('Antiproton', False),
+                          ('antiproton', False),
+                          ('p+', False),
+                          ('p--', False),
+                          ('P-', False),
+                          (57, False),
+                          ('alpha', True),
+                          ('He-4 2+', True),
+                          ('He-4++', True),
+                          ('He-3 2+', False),
+                          ('He-5 2+', False),
+                          ('Helium-4 +2', True),
+                          ('Helium-4 -2', False),
+                          ('He-4', False),
+                          ('helium', False),
+                          ('He', False),
+                          ('Fe-56', False),
+                          ('Fe', False),
+                          ('he-4 2+', False)])
+def test_is_alpha(test_input, expected):
+    assert _is_alpha(test_input) == expected
+
+
+@pytest.mark.parametrize("test_input,expected_newarg,expected_Z",
+                         [('H', 'H', None),
+                          ('H+', 'H', 1),
+                          ('D 1+', 'D', 1),
+                          ('alpha', 'alpha', 2),
+                          ('Fe', 'Fe', None),
+                          ('Titanium', 'Titanium', None),
+                          ('N-7+++', 'N-7', 3),
+                          ('H-1-', 'H-1', -1),
+                          ('He-4-', 'He-4', -1)])
+def test_extract_charge_state(test_input, expected_newarg, expected_Z):
+    new_symbol, new_Z = _extract_charge_state(test_input)
+    assert new_symbol == expected_newarg, str(test_input)
+    assert new_Z == expected_Z
+
+
+@pytest.mark.parametrize("test_input,expected_error",
+                         [('H-1-+-+', ValueError),
+                          ('H ++', ValueError),
+                          ('Fe +21+', ValueError),
+                          ('lead-12345', ValueError)])
+def test_extract_charge_state_errors(test_input, expected_error):
+    with pytest.raises(expected_error):
+        _extract_charge_state(test_input)
+
+
+@pytest.mark.parametrize("test_input,expected_warning",
+                         [('H-1----', UserWarning),
+                          ('Fe -4', UserWarning),
+                          ('lead 4-', UserWarning)])
+def test_extract_charge_state_errors(test_input, expected_warning):
+    with pytest.warns(expected_warning):
+        _extract_charge_state(test_input)
diff --git a/plasmapy/atomic/tests/test_nuclear.py b/plasmapy/atomic/tests/test_nuclear.py
new file mode 100644
index 00000000..b862aca6
--- /dev/null
+++ b/plasmapy/atomic/tests/test_nuclear.py
@@ -0,0 +1,81 @@
+from itertools import product
+from astropy import units as u, constants as const
+import numpy as np
+from ..nuclear import (nuclear_binding_energy, nuclear_reaction_energy)
+
+import pytest
+
+
+def test_nuclear_binding_energy():
+    assert nuclear_binding_energy('p') == 0
+    assert nuclear_binding_energy('n') == 0
+    assert nuclear_binding_energy('He-4') == nuclear_binding_energy('alpha') \
+        == nuclear_binding_energy('He', 4)
+
+
+def test_nuclear_binding_energy_D_T():
+    before = nuclear_binding_energy("D") + nuclear_binding_energy("T")
+    after = nuclear_binding_energy("alpha")
+    E_in_MeV = (after - before).to(u.MeV).value  # D + T --> alpha + n + E
+    assert np.isclose(E_in_MeV, 17.58, rtol=0.01)
+
+
+# (argument, expected_error)
+nuclear_binding_energy_table = [
+    ("H", ValueError),
+    (1.1, TypeError)]
+
+
+@pytest.mark.parametrize("argument, expected_error",
+                         nuclear_binding_energy_table)
+def test_nuclear_binding_energy_error(argument, expected_error):
+    with pytest.raises(expected_error):
+        nuclear_binding_energy(argument)
+
+
+def test_nuclear_reaction_energy():
+    reaction1 = 'D + T --> alpha + n'
+    reaction2 = 'T + D -> n + alpha'
+    released_energy1 = nuclear_reaction_energy(reaction1)
+    released_energy2 = nuclear_reaction_energy(reaction2)
+    assert np.isclose(released_energy1.to(u.MeV).value, 17.58, rtol=0.01)
+    assert released_energy1 == released_energy2
+
+
+def test_nuclear_reaction_energy_triple_alpha():
+    triple_alpha1 = 'alpha + 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)
+    assert np.isclose(energy_triplealpha1.to(u.keV).value, -91.8, atol=0.1)
+    assert np.isclose(energy_triplealpha2.to(u.MeV).value, 7.367, atol=0.1)
+
+
+def test_nuclear_reaction_energy_alpha_decay():
+    alpha_decay_example = 'U-238 --> Th-234 + alpha'
+    energy_alpha_decay = nuclear_reaction_energy(alpha_decay_example)
+    assert np.isclose(energy_alpha_decay.to(u.MeV).value, 4.26975, atol=1e-5)
+
+
+def test_nuclear_reaction_energy_triple_alpha_r():
+    triple_alpha1_r = '4He-4 --> 2Be-8'
+    energy_triplealpha1_r = nuclear_reaction_energy(triple_alpha1_r)
+    assert np.isclose(energy_triplealpha1_r.to(u.keV).value,
+                      -91.8 * 2, atol=0.1)
+
+
+# (reaction, expected_error)
+nuclear_reaction_energy_error_table = [
+    ('H + H --> H', ValueError),
+    (1, TypeError),
+    ('H-1 + H-1 --> H-1', ValueError),
+    ("I didn't like unstable eigenfunctions "
+     "at first, but then they grew on me", ValueError)
+]
+
+
+@pytest.mark.parametrize(
+    "reaction, expected_error", nuclear_reaction_energy_error_table)
+def test_nuclear_reaction_energy_error(reaction, expected_error):
+    with pytest.raises(expected_error):
+        nuclear_reaction_energy(reaction)
diff --git a/plasmapy/classes/tests/test_Species.py b/plasmapy/classes/tests/test_Species.py
index 22e6779b..7674355f 100644
--- a/plasmapy/classes/tests/test_Species.py
+++ b/plasmapy/classes/tests/test_Species.py
@@ -202,4 +202,4 @@ def test_particle_nonuniform_grid():
 
     plasma = Plasma(x, y, z)
 
-    Species(plasma, 'e', dt=1e-14*u.s, nt=5).run()
\ No newline at end of file
+    Species(plasma, 'e', dt=1e-14*u.s, nt=5).run()