Repository: PlasmaPy/PlasmaPy (python)

Issue:
Split up `_boilerPlate()` in `transport.py`
`_boilerPlate()` currently does a few different things like fetching particle data, calculating thermal velocity, tests/checks. Each piece of functionality should be split into its own function.

See #191 

Pull request description:
Split boiler plate, add mass argument to thermal_speed
Supersedes @apooravc 's #259 and closes #255 .

* Simplifies `_boilerPlate` via calling existing functionality. 
* Removes a bunch of tests now that we're testing their functionality in one centralized place
* Add a `mass` argument to `thermal_speed` (@lemmatum I think you wanted that?). I needed that o get _boilerPlate to work with the reduced mass.

Patch:
diff --git a/plasmapy/physics/parameters.py b/plasmapy/physics/parameters.py
index d2354c91..40745ed2 100644
--- a/plasmapy/physics/parameters.py
+++ b/plasmapy/physics/parameters.py
@@ -334,9 +334,11 @@ def ion_sound_speed(T_e,
 
 @utils.check_relativistic
 @utils.check_quantity({
-    'T': {'units': u.K, 'can_be_negative': False}
+    'T': {'units': u.K, 'can_be_negative': False},
+    'mass': {'units': u.kg, 'can_be_negative': False, 'can_be_nan': True}
     })
-def thermal_speed(T, particle="e-", method="most_probable"):
+@atomic.particle_input
+def thermal_speed(T, particle: atomic.Particle="e-", method="most_probable", mass=np.nan*u.kg):
     r"""
     Return the most probable speed for a particle within a Maxwellian
     distribution.
@@ -356,6 +358,11 @@ def thermal_speed(T, particle="e-", method="most_probable"):
         Method to be used for calculating the thermal speed. Options are
         `'most_probable'` (default), `'rms'`, and `'mean_magnitude'`.
 
+    mass : ~astropy.units.Quantity
+        The particle's mass override. Defaults to NaN and if so, doesn't do
+        anything, but if set, overrides mass acquired from `particle`. Useful
+        with relative velocities of particles.
+
     Returns
     -------
     V : ~astropy.units.Quantity
@@ -417,10 +424,7 @@ def thermal_speed(T, particle="e-", method="most_probable"):
 
     T = T.to(u.K, equivalencies=u.temperature_energy())
 
-    try:
-        m = atomic.particle_mass(particle)
-    except AtomicError:
-        raise ValueError("Unable to find {particle} mass in thermal_speed")
+    m = mass if np.isfinite(mass) else atomic.particle_mass(particle)
 
     # different methods, as per https://en.wikipedia.org/wiki/Thermal_velocity
     if method == "most_probable":
diff --git a/plasmapy/physics/transport/collisions.py b/plasmapy/physics/transport/collisions.py
index b061e316..82868951 100644
--- a/plasmapy/physics/transport/collisions.py
+++ b/plasmapy/physics/transport/collisions.py
@@ -7,14 +7,13 @@
 import warnings
 
 # plasmapy modules
-import plasmapy.atomic as atomic
 from plasmapy import utils
 from plasmapy.utils.checks import (check_quantity,
                                    _check_relativistic)
 
 from plasmapy.constants import (c, m_e, k_B, e, eps0, pi, hbar)
-from plasmapy.atomic import (particle_mass, integer_charge)
-from plasmapy.physics.parameters import (Debye_length)
+from plasmapy import atomic
+from plasmapy.physics import parameters
 from plasmapy.physics.quantum import (Wigner_Seitz_radius,
                                       thermal_deBroglie_wavelength,
                                       chemical_potential)
@@ -246,29 +245,16 @@ def _boilerPlate(T, particles, V):
                          "list or tuple containing representations of two  "
                          f"charged particles. Got {particles} instead.")
 
-    masses = np.zeros(2) * u.kg
-    charges = np.zeros(2) * u.C
-
-    for particle, i in zip(particles, range(2)):
-
-        try:
-            masses[i] = particle_mass(particles[i])
-        except Exception:
-            raise ValueError("Unable to find mass of particle: "
-                             f"{particles[i]}.")
-        try:
-            charges[i] = np.abs(e * integer_charge(particles[i]))
-            if charges[i] is None:
-                raise ValueError("Unable to find charge of particle: "
-                                 f"{particles[i]}.")
-        except Exception:
-            raise ValueError("Unable to find charge of particle: "
-                             f"{particles[i]}.")
+    particles = [atomic.Particle(p) for p in particles]
+    masses = [p.mass for p in particles]
+    charges = [np.abs(p.charge) for p in particles]
+
     # obtaining reduced mass of 2 particle collision system
-    reduced_mass = masses[0] * masses[1] / (masses[0] + masses[1])
+    reduced_mass = atomic.reduced_mass(*particles)
+
     # getting thermal velocity of system if no velocity is given
     if np.isnan(V):
-        V = np.sqrt(2 * k_B * T / reduced_mass).to(u.m / u.s)
+        V = parameters.thermal_speed(T, mass=reduced_mass)
     _check_relativistic(V, 'V')
     return T, masses, charges, reduced_mass, V
 
@@ -485,7 +471,7 @@ def impact_parameter(T,
             raise ValueError("Must provide a z_mean for GMS-2, GMS-5, and "
                              "GMS-6 methods.")
     # Debye length
-    lambdaDe = Debye_length(T, n_e)
+    lambdaDe = parameters.Debye_length(T, n_e)
     # deBroglie wavelength
     lambdaBroglie = hbar / (2 * reduced_mass * V)
     # distance of closest approach in 90 degree Coulomb collision


Test patch:
diff --git a/plasmapy/physics/tests/test_distribution.py b/plasmapy/physics/tests/test_distribution.py
index f4c3f3ba..35e7ffb0 100644
--- a/plasmapy/physics/tests/test_distribution.py
+++ b/plasmapy/physics/tests/test_distribution.py
@@ -104,16 +104,6 @@ def test_std(self):
         T_distri = (std**2 / k_B * m_e).to(u.K)
         assert np.isclose(T_distri.value, self.T_e.value)
 
-    def test_valErr(self):
-        """
-        Tests whether ValueError is raised when invalid particle name
-        string is passed.
-        """
-        with pytest.raises(ValueError):
-            Maxwellian_1D(1 * u.m / u.s,
-                          T=1 * u.K,
-                          particle='XXX')
-
     def test_units_no_vTh(self):
         """
         Tests distribution function with units, but not passing vTh.
@@ -813,17 +803,6 @@ def test_std(self):
         T_distri = (std**2 / k_B * m_e).to(u.K)
         assert np.isclose(T_distri.value, self.T_e.value)
 
-    def test_valErr(self):
-        """
-        Tests whether ValueError is raised when invalid particle name
-        string is passed.
-        """
-        with pytest.raises(ValueError):
-            kappa_velocity_1D(1 * u.m / u.s,
-                              T=1 * u.K,
-                              kappa=self.kappa,
-                              particle='XXX')
-
     def test_units_no_vTh(self):
         """
         Tests distribution function with units, but not passing vTh.
diff --git a/plasmapy/physics/tests/test_parameters.py b/plasmapy/physics/tests/test_parameters.py
index c9a0e5f5..30ebb26a 100644
--- a/plasmapy/physics/tests/test_parameters.py
+++ b/plasmapy/physics/tests/test_parameters.py
@@ -305,7 +305,7 @@ def test_thermal_speed():
     with pytest.raises(RelativityError):
         thermal_speed(1e14 * u.K, particle='p')
 
-    with pytest.raises(ValueError):
+    with pytest.raises(InvalidParticleError):
         thermal_speed(T_i, particle='asdfasd')
 
     with pytest.warns(u.UnitsWarning):
@@ -561,9 +561,6 @@ def test_gyroradius():
     with pytest.raises(TypeError):
         gyroradius(u.T, particle="p", Vperp=8 * u.m / u.s)
 
-    with pytest.raises(ValueError):
-        gyroradius(B, particle='asfdas', T_i=T_i)
-
     with pytest.raises(ValueError):
         gyroradius(B, particle='p', T_i=-1 * u.K)
 
diff --git a/plasmapy/physics/transport/tests/test_collisions.py b/plasmapy/physics/transport/tests/test_collisions.py
index bc60f0f4..5a2ceaa6 100644
--- a/plasmapy/physics/transport/tests/test_collisions.py
+++ b/plasmapy/physics/transport/tests/test_collisions.py
@@ -11,7 +11,7 @@
                                         Knudsen_number,
                                         coupling_parameter)
 from plasmapy.physics.transport.collisions import Spitzer_resistivity
-from plasmapy.utils import RelativityWarning, RelativityError, PhysicsWarning
+from plasmapy.utils import exceptions
 from plasmapy.constants import m_p, m_e, c
 
 
@@ -117,7 +117,7 @@ def test_Chen_fusion(self):
         # velocity. Chen uses v**2 = k * T / m  whereas we use
         # v ** 2 = 2 * k * T / m
         lnLambdaChen = 16 + np.log(2)
-        with pytest.warns(RelativityWarning):
+        with pytest.warns(exceptions.RelativityWarning):
             lnLambda = Coulomb_logarithm(T, n, ('e', 'p'))
         testTrue = np.isclose(lnLambda,
                               lnLambdaChen,
@@ -140,7 +140,7 @@ def test_Chen_laser(self):
         # velocity. Chen uses v**2 = k * T / m  whereas we use
         # v ** 2 = 2 * k * T / m
         lnLambdaChen = 6.8 + np.log(2)
-        with pytest.warns(RelativityWarning):
+        with pytest.warns(exceptions.RelativityWarning):
             lnLambda = Coulomb_logarithm(T, n, ('e', 'p'))
         testTrue = np.isclose(lnLambda,
                               lnLambdaChen,
@@ -155,7 +155,7 @@ def test_GMS1(self):
         Test for first version of Coulomb logarithm from Gericke,
         Murillo, and Schlanges PRE (2002).
         """
-        with pytest.warns(PhysicsWarning, match="strong coupling effects"):
+        with pytest.warns(exceptions.PhysicsWarning, match="strong coupling effects"):
             methodVal = Coulomb_logarithm(self.temperature1,
                                           self.density1,
                                           self.particles,
@@ -176,7 +176,7 @@ def test_GMS1_negative(self):
         Murillo, and Schlanges PRE (2002). This checks for when
         a negative (invalid) Coulomb logarithm is returned.
         """
-        with pytest.warns(PhysicsWarning, match="strong coupling effects"):
+        with pytest.warns(exceptions.PhysicsWarning, match="strong coupling effects"):
             methodVal = Coulomb_logarithm(self.temperature2,
                                           self.density2,
                                           self.particles,
@@ -196,7 +196,7 @@ def test_GMS2(self):
         Test for second version of Coulomb logarithm from Gericke,
         Murillo, and Schlanges PRE (2002).
         """
-        with pytest.warns(PhysicsWarning, match="strong coupling effects"):
+        with pytest.warns(exceptions.PhysicsWarning, match="strong coupling effects"):
             methodVal = Coulomb_logarithm(self.temperature1,
                                           self.density1,
                                           self.particles,
@@ -217,7 +217,7 @@ def test_GMS2_negative(self):
         Murillo, and Schlanges PRE (2002). This checks for when
         a negative (invalid) Coulomb logarithm is returned.
         """
-        with pytest.warns(PhysicsWarning, match="strong coupling effects"):
+        with pytest.warns(exceptions.PhysicsWarning, match="strong coupling effects"):
             methodVal = Coulomb_logarithm(self.temperature2,
                                           self.density2,
                                           self.particles,
@@ -237,7 +237,7 @@ def test_GMS3(self):
         Test for third version of Coulomb logarithm from Gericke,
         Murillo, and Schlanges PRE (2002).
         """
-        with pytest.warns(PhysicsWarning, match="strong coupling effects"):
+        with pytest.warns(exceptions.PhysicsWarning, match="strong coupling effects"):
             methodVal = Coulomb_logarithm(self.temperature1,
                                           self.density1,
                                           self.particles,
@@ -259,7 +259,7 @@ def test_GMS3_negative(self):
         a positive value is returned whereas the classical Coulomb
         logarithm would return a negative value.
         """
-        with pytest.warns(PhysicsWarning, match="strong coupling effects"):
+        with pytest.warns(exceptions.PhysicsWarning, match="strong coupling effects"):
             methodVal = Coulomb_logarithm(self.temperature2,
                                           self.density2,
                                           self.particles,
@@ -279,7 +279,7 @@ def test_GMS4(self):
         Test for fourth version of Coulomb logarithm from Gericke,
         Murillo, and Schlanges PRE (2002).
         """
-        with pytest.warns(PhysicsWarning, match="strong coupling effects"):
+        with pytest.warns(exceptions.PhysicsWarning, match="strong coupling effects"):
             methodVal = Coulomb_logarithm(self.temperature1,
                                           self.density1,
                                           self.particles,
@@ -301,7 +301,7 @@ def test_GMS4_negative(self):
         a positive value is returned whereas the classical Coulomb
         logarithm would return a negative value.
         """
-        with pytest.warns(PhysicsWarning, match="strong coupling effects"):
+        with pytest.warns(exceptions.PhysicsWarning, match="strong coupling effects"):
             methodVal = Coulomb_logarithm(self.temperature2,
                                           self.density2,
                                           self.particles,
@@ -321,7 +321,7 @@ def test_GMS5(self):
         Test for fifth version of Coulomb logarithm from Gericke,
         Murillo, and Schlanges PRE (2002).
         """
-        with pytest.warns(PhysicsWarning, match="strong coupling effects"):
+        with pytest.warns(exceptions.PhysicsWarning, match="strong coupling effects"):
             methodVal = Coulomb_logarithm(self.temperature1,
                                           self.density1,
                                           self.particles,
@@ -343,7 +343,7 @@ def test_GMS5_negative(self):
         a positive value is returned whereas the classical Coulomb
         logarithm would return a negative value.
         """
-        with pytest.warns(PhysicsWarning, match="strong coupling effects"):
+        with pytest.warns(exceptions.PhysicsWarning, match="strong coupling effects"):
             methodVal = Coulomb_logarithm(self.temperature2,
                                           self.density2,
                                           self.particles,
@@ -363,7 +363,7 @@ def test_GMS6(self):
         Test for sixth version of Coulomb logarithm from Gericke,
         Murillo, and Schlanges PRE (2002).
         """
-        with pytest.warns(PhysicsWarning, match="strong coupling effects"):
+        with pytest.warns(exceptions.PhysicsWarning, match="strong coupling effects"):
             methodVal = Coulomb_logarithm(self.temperature1,
                                           self.density1,
                                           self.particles,
@@ -385,7 +385,7 @@ def test_GMS6_negative(self):
         a positive value is returned whereas the classical Coulomb
         logarithm would return a negative value.
         """
-        with pytest.warns(PhysicsWarning, match="strong coupling effects"):
+        with pytest.warns(exceptions.PhysicsWarning, match="strong coupling effects"):
             methodVal = Coulomb_logarithm(self.temperature2,
                                           self.density2,
                                           self.particles,
@@ -435,12 +435,12 @@ def test_GMS6_zmean_error(self):
 
     def test_relativity_warn(self):
         """Tests whether relativity warning is raised at high velocity."""
-        with pytest.warns(RelativityWarning):
+        with pytest.warns(exceptions.RelativityWarning):
             Coulomb_logarithm(1e5 * u.K, 1 * u.m ** -3, ('e', 'p'), V=0.9 * c)
 
     def test_relativity_error(self):
         """Tests whether relativity error is raised at light speed."""
-        with pytest.raises(RelativityError):
+        with pytest.raises(exceptions.RelativityError):
             Coulomb_logarithm(1e5 * u.K, 1 * u.m ** -3, ('e', 'p'), V=1.1 * c)
 
     def test_unit_conversion_error(self):
@@ -464,7 +464,7 @@ def test_invalid_particle_error(self):
         Tests whether an error is raised when an invalid particle name
         is given.
         """
-        with pytest.raises(ValueError):
+        with pytest.raises(exceptions.InvalidParticleError):
             Coulomb_logarithm(1 * u.K, 5 * u.m ** -3, ('e', 'g'))
 
     n_e = np.array([1e9, 1e9, 1e24]) * u.cm ** -3
@@ -605,7 +605,7 @@ def test_known1(self):
         """
         Test for known value.
         """
-        with pytest.warns(PhysicsWarning, match="strong coupling effects"):
+        with pytest.warns(exceptions.PhysicsWarning, match="strong coupling effects"):
             methodVal = collision_frequency(self.T,
                                             self.n,
                                             self.particles,
@@ -626,7 +626,7 @@ def test_fail1(self):
         value comparison by some quantity close to numerical error.
         """
         fail1 = self.True1 * (1 + 1e-15)
-        with pytest.warns(PhysicsWarning, match="strong coupling effects"):
+        with pytest.warns(exceptions.PhysicsWarning, match="strong coupling effects"):
             methodVal = collision_frequency(self.T,
                                             self.n,
                                             self.particles,
@@ -645,7 +645,7 @@ def test_electrons(self):
         """
         Testing collision frequency between electrons.
         """
-        with pytest.warns(PhysicsWarning, match="strong coupling effects"):
+        with pytest.warns(exceptions.PhysicsWarning, match="strong coupling effects"):
             methodVal = collision_frequency(self.T,
                                             self.n,
                                             self.electrons,
@@ -664,7 +664,7 @@ def test_protons(self):
         """
         Testing collision frequency between protons (ions).
         """
-        with pytest.warns(PhysicsWarning, match="strong coupling effects"):
+        with pytest.warns(exceptions.PhysicsWarning, match="strong coupling effects"):
             methodVal = collision_frequency(self.T,
                                             self.n,
                                             self.protons,
@@ -683,7 +683,7 @@ def test_zmean(self):
         """
         Test collisional frequency function when given arbitrary z_mean.
         """
-        with pytest.warns(PhysicsWarning, match="strong coupling effects"):
+        with pytest.warns(exceptions.PhysicsWarning, match="strong coupling effects"):
             methodVal = collision_frequency(self.T,
                                             self.n,
                                             self.particles,
@@ -714,7 +714,7 @@ def test_known1(self):
         """
         Test for known value.
         """
-        with pytest.warns(PhysicsWarning, match="strong coupling effects"):
+        with pytest.warns(exceptions.PhysicsWarning, match="strong coupling effects"):
             methodVal = mean_free_path(self.T,
                                        self.n_e,
                                        self.particles,
@@ -735,7 +735,7 @@ def test_fail1(self):
         value comparison by some quantity close to numerical error.
         """
         fail1 = self.True1 * (1 + 1e-15)
-        with pytest.warns(PhysicsWarning, match="strong coupling effects"):
+        with pytest.warns(exceptions.PhysicsWarning, match="strong coupling effects"):
             methodVal = mean_free_path(self.T,
                                        self.n_e,
                                        self.particles,
@@ -834,7 +834,7 @@ def test_known1(self):
         """
         Test for known value.
         """
-        with pytest.warns(PhysicsWarning, match="strong coupling effects"):
+        with pytest.warns(exceptions.PhysicsWarning, match="strong coupling effects"):
             methodVal = mobility(self.T,
                                  self.n_e,
                                  self.particles,
@@ -855,7 +855,7 @@ def test_fail1(self):
         value comparison by some quantity close to numerical error.
         """
         fail1 = self.True1 * (1 + 1e-15)
-        with pytest.warns(PhysicsWarning, match="strong coupling effects"):
+        with pytest.warns(exceptions.PhysicsWarning, match="strong coupling effects"):
             methodVal = mobility(self.T,
                                  self.n_e,
                                  self.particles,
@@ -872,7 +872,7 @@ def test_fail1(self):
 
     def test_zmean(self):
         """Testing mobility when z_mean is passed."""
-        with pytest.warns(PhysicsWarning, match="strong coupling effects"):
+        with pytest.warns(exceptions.PhysicsWarning, match="strong coupling effects"):
             methodVal = mobility(self.T,
                                  self.n_e,
                                  self.particles,
@@ -904,7 +904,7 @@ def test_known1(self):
         """
         Test for known value.
         """
-        with pytest.warns(PhysicsWarning, match="strong coupling effects"):
+        with pytest.warns(exceptions.PhysicsWarning, match="strong coupling effects"):
             methodVal = Knudsen_number(self.length,
                                        self.T,
                                        self.n_e,
@@ -926,7 +926,7 @@ def test_fail1(self):
         value comparison by some quantity close to numerical error.
         """
         fail1 = self.True1 * (1 + 1e-15)
-        with pytest.warns(PhysicsWarning, match="strong coupling effects"):
+        with pytest.warns(exceptions.PhysicsWarning, match="strong coupling effects"):
             methodVal = Knudsen_number(self.length,
                                        self.T,
                                        self.n_e,