Repository: PlasmaPy/PlasmaPy

Issue:
Move `mass_density` from `misc.py` to `densities.py`
We currently have a function for `mass_density` in `plasmapy.formulary.misc`. To keep with the scheme where `plasmapy.formulary` functions are broken down by physical type, we might want to move this to `plasmapy.formulary.densities` instead.  

Some things to think about with this:
 - The mass density has a different physical type than number densities
 - Mass density is a bit conceptually different than other formulary functions.  I'd almost describe mass density as a plasma property rather than a plasma parameter.  This might perhaps have been why it ended up going into `misc.py`.


Discussion:
I can take this as my first contribution.

Resolving patch:
diff --git a/plasmapy/formulary/densities.py b/plasmapy/formulary/densities.py
--- a/plasmapy/formulary/densities.py
+++ b/plasmapy/formulary/densities.py
@@ -1,14 +1,21 @@
 """Functions to calculate plasma density parameters."""
 __all__ = [
     "critical_density",
+    "mass_density",
 ]
-
+__aliases__ = ["rho_"]
 import astropy.units as u
+import numbers
+import numpy as np
 
 from astropy.constants.si import e, eps0, m_e
+from typing import Optional
 
+from plasmapy.particles import Particle, ParticleLike
 from plasmapy.utils.decorators import validate_quantities
 
+__all__ += __aliases__
+
 
 @validate_quantities(
     omega={"can_be_negative": False},
@@ -55,3 +62,107 @@ def critical_density(omega: u.rad / u.s) -> u.m**-3:
     n_c = m_e * eps0 * omega**2 / (e**2)
 
     return n_c.to(u.m**-3, equivalencies=u.dimensionless_angles())
+
+
+@validate_quantities(
+    density={"can_be_negative": False}, validations_on_return={"can_be_negative": False}
+)
+def mass_density(
+    density: (u.m**-3, u.kg / (u.m**3)),
+    particle: ParticleLike,
+    z_ratio: Optional[numbers.Real] = 1,
+) -> u.kg / u.m**3:
+    r"""
+    Calculate the mass density from a number density.
+
+    .. math::
+
+        \rho = \left| \frac{Z_{s}}{Z_{particle}} \right| n_{s} m_{particle}
+              = | Z_{ratio} | n_{s} m_{particle}
+
+    where :math:`m_{particle}` is the particle mass, :math:`n_{s}` is a number
+    density for plasma species :math:`s`, :math:`Z_{s}` is the charge number of
+    species :math:`s`, and :math:`Z_{particle}` is the charge number of
+    ``particle``.  For example, if the electron density is given for :math:`n_s`
+    and ``particle`` is a doubly ionized atom, then :math:`Z_{ratio} = -1 / 2`\ .
+
+    Parameters
+    ----------
+    density : `~astropy.units.Quantity`
+        Either a particle number density (in units of m\ :sup:`-3` or
+        equivalent) or a mass density (in units of kg / m\ :sup:`3` or
+        equivalent).  If ``density`` is a mass density, then it will be passed
+        through and returned without modification.
+
+    particle : `~plasmapy.particles.particle_class.Particle`
+        The particle for which the mass density is being calculated for.  Must
+        be a `~plasmapy.particles.particle_class.Particle` or a value convertible to
+        a `~plasmapy.particles.particle_class.Particle` (e.g., ``'p'`` for protons,
+        ``'D+'`` for deuterium, or ``'He-4 +1'`` for singly ionized helium-4).
+
+    z_ratio : `int`, `float`, optional
+        The ratio of the charge numbers corresponding to the plasma species
+        represented by ``density`` and the ``particle``.  For example, if the
+        given ``density`` is and electron density and ``particle`` is doubly
+        ionized ``He``, then ``z_ratio = -0.5``.  Default is ``1``.
+
+    Raises
+    ------
+    `~astropy.units.UnitTypeError`
+        If the ``density`` does not have units equivalent to a number density
+        or mass density.
+
+    `TypeError`
+        If ``density`` is not of type `~astropy.units.Quantity`, or convertible.
+
+    `TypeError`
+        If ``particle`` is not of type or convertible to
+        `~plasmapy.particles.particle_class.Particle`.
+
+    `TypeError`
+        If ``z_ratio`` is not of type `int` or `float`.
+
+    `ValueError`
+        If ``density`` is negative.
+
+    Returns
+    -------
+    `~astropy.units.Quantity`
+        The mass density for the plasma species represented by ``particle``.
+
+    Examples
+    --------
+    >>> import astropy.units as u
+    >>> mass_density(1 * u.m ** -3, 'p')
+    <Quantity 1.67262...e-27 kg / m3>
+    >>> mass_density(4 * u.m ** -3, 'D+')
+    <Quantity 1.33743...e-26 kg / m3>
+    >>> mass_density(2.e12 * u.cm ** -3, 'He')
+    <Quantity 1.32929...e-08 kg / m3>
+    >>> mass_density(2.e12 * u.cm ** -3, 'He', z_ratio=0.5)
+    <Quantity 6.64647...e-09 kg / m3>
+    >>> mass_density(1.0 * u.g * u.m ** -3, "")
+    <Quantity 0.001 kg / m3>
+    """
+    if density.unit.is_equivalent(u.kg / u.m**3):
+        return density
+
+    if not isinstance(particle, Particle):
+        try:
+            particle = Particle(particle)
+        except TypeError as e:
+            raise TypeError(
+                f"If passing a number density, you must pass a plasmapy Particle "
+                f"(not type {type(particle)}) to calculate the mass density!"
+            ) from e
+
+    if not isinstance(z_ratio, (float, np.floating, int, np.integer)):
+        raise TypeError(
+            f"Expected type int or float for keyword z_ratio, got type {type(z_ratio)}."
+        )
+
+    return abs(z_ratio) * density * particle.mass
+
+
+rho_ = mass_density
+"""Alias to `~plasmapy.formulary.densities.mass_density`."""
diff --git a/plasmapy/formulary/misc.py b/plasmapy/formulary/misc.py
--- a/plasmapy/formulary/misc.py
+++ b/plasmapy/formulary/misc.py
@@ -4,20 +4,16 @@
     "Bohm_diffusion",
     "magnetic_energy_density",
     "magnetic_pressure",
-    "mass_density",
     "thermal_pressure",
 ]
-__aliases__ = ["DB_", "pmag_", "pth_", "rho_", "ub_"]
+__aliases__ = ["DB_", "pmag_", "pth_", "ub_"]
 
 import astropy.units as u
-import numbers
-import numpy as np
 
 from astropy.constants.si import e, k_B, mu0
-from typing import Optional
 
 from plasmapy import particles
-from plasmapy.particles import Particle, ParticleLike
+from plasmapy.particles import ParticleLike
 from plasmapy.utils.decorators import validate_quantities
 
 __all__ += __aliases__
@@ -247,112 +243,6 @@ def magnetic_pressure(B: u.T) -> u.Pa:
 """Alias to `~plasmapy.formulary.misc.magnetic_pressure`."""
 
 
-@validate_quantities(
-    density={"can_be_negative": False}, validations_on_return={"can_be_negative": False}
-)
-def mass_density(
-    density: (u.m**-3, u.kg / (u.m**3)),
-    particle: ParticleLike,
-    z_ratio: Optional[numbers.Real] = 1,
-) -> u.kg / u.m**3:
-    r"""
-    Calculate the mass density from a number density.
-
-    .. math::
-
-        \rho = \left| \frac{Z_{s}}{Z_{particle}} \right| n_{s} m_{particle}
-              = | Z_{ratio} | n_{s} m_{particle}
-
-    where :math:`m_{particle}` is the particle mass, :math:`n_{s}` is a number
-    density for plasma species :math:`s`, :math:`Z_{s}` is the charge number of
-    species :math:`s`, and :math:`Z_{particle}` is the charge number of
-    ``particle``.  For example, if the electron density is given for :math:`n_s`
-    and ``particle`` is a doubly ionized atom, then :math:`Z_{ratio} = -1 / 2`\ .
-
-    **Aliases:** `rho_`
-
-    Parameters
-    ----------
-    density : `~astropy.units.Quantity`
-        Either a particle number density (in units of m\ :sup:`-3` or
-        equivalent) or a mass density (in units of kg / m\ :sup:`3` or
-        equivalent).  If ``density`` is a mass density, then it will be passed
-        through and returned without modification.
-
-    particle : `~plasmapy.particles.particle_class.Particle`
-        The particle for which the mass density is being calculated for.  Must
-        be a `~plasmapy.particles.particle_class.Particle` or a value convertible to
-        a `~plasmapy.particles.particle_class.Particle` (e.g., ``'p'`` for protons,
-        ``'D+'`` for deuterium, or ``'He-4 +1'`` for singly ionized helium-4).
-
-    z_ratio : `int`, `float`, optional
-        The ratio of the charge numbers corresponding to the plasma species
-        represented by ``density`` and the ``particle``.  For example, if the
-        given ``density`` is and electron density and ``particle`` is doubly
-        ionized ``He``, then ``z_ratio = -0.5``.  Default is ``1``.
-
-    Raises
-    ------
-    `~astropy.units.UnitTypeError`
-        If the ``density`` does not have units equivalent to a number density
-        or mass density.
-
-    `TypeError`
-        If ``density`` is not of type `~astropy.units.Quantity`, or convertible.
-
-    `TypeError`
-        If ``particle`` is not of type or convertible to
-        `~plasmapy.particles.particle_class.Particle`.
-
-    `TypeError`
-        If ``z_ratio`` is not of type `int` or `float`.
-
-    `ValueError`
-        If ``density`` is negative.
-
-    Returns
-    -------
-    `~astropy.units.Quantity`
-        The mass density for the plasma species represented by ``particle``.
-
-    Examples
-    --------
-    >>> import astropy.units as u
-    >>> mass_density(1 * u.m ** -3, 'p')
-    <Quantity 1.67262...e-27 kg / m3>
-    >>> mass_density(4 * u.m ** -3, 'D+')
-    <Quantity 1.33743...e-26 kg / m3>
-    >>> mass_density(2.e12 * u.cm ** -3, 'He')
-    <Quantity 1.32929...e-08 kg / m3>
-    >>> mass_density(2.e12 * u.cm ** -3, 'He', z_ratio=0.5)
-    <Quantity 6.64647...e-09 kg / m3>
-    >>> mass_density(1.0 * u.g * u.m ** -3, "")
-    <Quantity 0.001 kg / m3>
-    """
-    if density.unit.is_equivalent(u.kg / u.m**3):
-        return density
-
-    if not isinstance(particle, Particle):
-        try:
-            particle = Particle(particle)
-        except TypeError as e:
-            raise TypeError(
-                f"If passing a number density, you must pass a plasmapy Particle "
-                f"(not type {type(particle)}) to calculate the mass density!"
-            ) from e
-
-    if not isinstance(z_ratio, (float, np.floating, int, np.integer)):
-        raise TypeError(
-            f"Expected type int or float for keyword z_ratio, got type {type(z_ratio)}."
-        )
-
-    return abs(z_ratio) * density * particle.mass
-
-
-rho_ = mass_density
-"""Alias to `~plasmapy.formulary.misc.mass_density`."""
-
-
 @validate_quantities(
     T={"can_be_negative": False, "equivalencies": u.temperature_energy()},
     n={"can_be_negative": False},


Test patch:
diff --git a/plasmapy/formulary/tests/test_densities.py b/plasmapy/formulary/tests/test_densities.py
--- a/plasmapy/formulary/tests/test_densities.py
+++ b/plasmapy/formulary/tests/test_densities.py
@@ -3,8 +3,10 @@
 import numpy as np
 import pytest
 
-from plasmapy.formulary.densities import critical_density
+from plasmapy.formulary.densities import critical_density, mass_density
 from plasmapy.formulary.frequencies import plasma_frequency
+from plasmapy.particles import Particle
+from plasmapy.utils._pytest_helpers import assert_can_handle_nparray
 
 
 class TestCriticalDensity:
@@ -33,3 +35,56 @@ def test_value(self, n_c):
         """
 
         assert np.isclose(n_c.value, self.n_i.value)
+
+
+class Test_mass_density:
+    r"""Test the mass_density function in densities.py."""
+
+    @pytest.mark.parametrize(
+        ("args", "kwargs", "conditional"),
+        [
+            ((-1 * u.kg * u.m**-3, "He"), {}, pytest.raises(ValueError)),
+            ((-1 * u.m**-3, "He"), {}, pytest.raises(ValueError)),
+            (("not a Quantity", "He"), {}, pytest.raises(TypeError)),
+            ((1 * u.m**-3,), {}, pytest.raises(TypeError)),
+            ((1 * u.J, "He"), {}, pytest.raises(u.UnitTypeError)),
+            ((1 * u.m**-3, None), {}, pytest.raises(TypeError)),
+            (
+                (1 * u.m**-3, "He"),
+                {"z_ratio": "not a ratio"},
+                pytest.raises(TypeError),
+            ),
+        ],
+    )
+    def test_raises(self, args, kwargs, conditional):
+        with conditional:
+            mass_density(*args, **kwargs)
+
+    @pytest.mark.parametrize(
+        ("args", "kwargs", "expected"),
+        [
+            ((1.0 * u.g * u.m**-3, ""), {}, 1.0e-3 * u.kg * u.m**-3),
+            ((5.0e12 * u.cm**-3, "He"), {}, 3.32323849e-8 * u.kg * u.m**-3),
+            (
+                (5.0e12 * u.cm**-3, Particle("He")),
+                {},
+                3.32323849e-8 * u.kg * u.m**-3,
+            ),
+            (
+                (5.0e12 * u.cm**-3, "He"),
+                {"z_ratio": 0.5},
+                1.66161925e-08 * u.kg * u.m**-3,
+            ),
+            (
+                (5.0e12 * u.cm**-3, "He"),
+                {"z_ratio": -0.5},
+                1.66161925e-08 * u.kg * u.m**-3,
+            ),
+        ],
+    )
+    def test_values(self, args, kwargs, expected):
+        assert np.isclose(mass_density(*args, **kwargs), expected)
+
+    def test_handle_nparrays(self):
+        """Test for ability to handle numpy array quantities"""
+        assert_can_handle_nparray(mass_density)
diff --git a/plasmapy/formulary/tests/test_misc.py b/plasmapy/formulary/tests/test_misc.py
--- a/plasmapy/formulary/tests/test_misc.py
+++ b/plasmapy/formulary/tests/test_misc.py
@@ -11,14 +11,11 @@
     DB_,
     magnetic_energy_density,
     magnetic_pressure,
-    mass_density,
     pmag_,
     pth_,
-    rho_,
     thermal_pressure,
     ub_,
 )
-from plasmapy.particles import Particle
 from plasmapy.utils._pytest_helpers import assert_can_handle_nparray
 
 B = 1.0 * u.T
@@ -36,7 +33,6 @@
         (DB_, Bohm_diffusion),
         (ub_, magnetic_energy_density),
         (pmag_, magnetic_pressure),
-        (rho_, mass_density),
         (pth_, thermal_pressure),
     ],
 )
@@ -45,59 +41,6 @@ def test_aliases(alias, parent):
     assert alias is parent
 
 
-class Test_mass_density:
-    r"""Test the mass_density function in misc.py."""
-
-    @pytest.mark.parametrize(
-        ("args", "kwargs", "conditional"),
-        [
-            ((-1 * u.kg * u.m**-3, "He"), {}, pytest.raises(ValueError)),
-            ((-1 * u.m**-3, "He"), {}, pytest.raises(ValueError)),
-            (("not a Quantity", "He"), {}, pytest.raises(TypeError)),
-            ((1 * u.m**-3,), {}, pytest.raises(TypeError)),
-            ((1 * u.J, "He"), {}, pytest.raises(u.UnitTypeError)),
-            ((1 * u.m**-3, None), {}, pytest.raises(TypeError)),
-            (
-                (1 * u.m**-3, "He"),
-                {"z_ratio": "not a ratio"},
-                pytest.raises(TypeError),
-            ),
-        ],
-    )
-    def test_raises(self, args, kwargs, conditional):
-        with conditional:
-            mass_density(*args, **kwargs)
-
-    @pytest.mark.parametrize(
-        ("args", "kwargs", "expected"),
-        [
-            ((1.0 * u.g * u.m**-3, ""), {}, 1.0e-3 * u.kg * u.m**-3),
-            ((5.0e12 * u.cm**-3, "He"), {}, 3.32323849e-8 * u.kg * u.m**-3),
-            (
-                (5.0e12 * u.cm**-3, Particle("He")),
-                {},
-                3.32323849e-8 * u.kg * u.m**-3,
-            ),
-            (
-                (5.0e12 * u.cm**-3, "He"),
-                {"z_ratio": 0.5},
-                1.66161925e-08 * u.kg * u.m**-3,
-            ),
-            (
-                (5.0e12 * u.cm**-3, "He"),
-                {"z_ratio": -0.5},
-                1.66161925e-08 * u.kg * u.m**-3,
-            ),
-        ],
-    )
-    def test_values(self, args, kwargs, expected):
-        assert np.isclose(mass_density(*args, **kwargs), expected)
-
-    def test_handle_nparrays(self):
-        """Test for ability to handle numpy array quantities"""
-        assert_can_handle_nparray(mass_density)
-
-
 def test_thermal_pressure():
     assert thermal_pressure(T_e, n_i).unit.is_equivalent(u.Pa)