Repository: PlasmaPy/PlasmaPy

Issue:
Create a CodeMeta instance file
The [CodeMeta Project](https://codemeta.github.io/) is an effort to standardize metadata for different software projects that I learned about recently from the head librarian where I work.  As I understand it, we create a JSON file in our top-level directory using the tools provided on the website.  This file will contain the metadata describing PlasmaPy.  The CodeMeta standard is changing and our metadata will keep changing too, so we'll probably need to include updates to this in our release guide.

I'll tentatively assign myself to this since I can ask the librarian here for help, but the resulting file should be double checked by several of us.  We can schedule this for 0.2 since it shouldn't be too much extra effort and metadata is important.

Discussion:
namurphy: The codemeta standard is still relatively new and will probably evolve, so we should make sure to check before each release to make sure the standard hasn't changed.  

We might want to add a separate issue to add in all of the contributors.  Hopefully, metadata in codemeta.json will be able to be read in by Zenodo to autofill a bunch of the things, like the list of contributors.

Patch:
diff --git a/changelog/667.feature.rst b/changelog/667.feature.rst
new file mode 100644
index 00000000..806c5f37
--- /dev/null
+++ b/changelog/667.feature.rst
@@ -0,0 +1,1 @@
+Create a decorator to change output of physics functions from "radians/s" to "hz"
diff --git a/changelog/676.trivial.rst b/changelog/676.trivial.rst
new file mode 100644
index 00000000..e68343df
--- /dev/null
+++ b/changelog/676.trivial.rst
@@ -0,0 +1,1 @@
+Add a Codemeta file (``codemeta.json``)
diff --git a/codemeta.json b/codemeta.json
new file mode 100644
index 00000000..f8951af0
--- /dev/null
+++ b/codemeta.json
@@ -0,0 +1,55 @@
+{
+  "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
+  "@type": "SoftwareSourceCode",
+  "name": "PlasmaPy",
+  "description": "An open source software package for plasma research and education",
+  "keywords": [
+    "plasma physics",
+    "open source",
+    "scientific software",
+    "Python"
+  ],
+  "version": "0.3.0",
+  "license": "https://github.com/PlasmaPy/PlasmaPy/blob/master/LICENSE.md",
+  "creator": [
+    {
+      "@type": "Organization",
+      "name": "PlasmaPy Community"
+    },
+    {}
+  ],
+  "codeRepository": "https://github.com/PlasmaPy/PlasmaPy",
+  "programmingLanguage": [
+    "Python 3.6",
+    "Python 3.7"
+  ],
+  "copyrightHolder": "PlasmaPy Developers",
+  "isAccessibleForFree": true,
+  "issueTracker": "https://github.com/PlasmaPy/plasmapy/issues",
+  "readme": "https://github.com/PlasmaPy/PlasmaPy/blob/master/README.md",
+  "developmentStatus": "WIP",
+  "buildInstructions": "http://docs.plasmapy.org/en/stable/install.html",
+  "referencePublication": "https://doi.org/10.5281/zenodo.1238132",
+  "funder": [
+    {
+      "@id": "https://doi.org/10.13039/100000001",
+      "@type": "Organization",
+      "name": "National Science Foundation"
+    },
+    {
+      "@id": "https://doi.org/10.13039/100000015",
+      "@type": "Organization",
+      "name": "U.S. Department of Energy"
+    },
+    {
+      "@id": "https://doi.org/10.03039/100000104",
+      "@type": "Organization",
+      "name": "NASA"
+    },
+    {
+      "@id": "https://doi.org/10.03039/100000014",
+      "@type": "Organization",
+      "name": "Smithsonian Institution"
+    }
+  ]
+}
diff --git a/docs/development/release_guide.rst b/docs/development/release_guide.rst
index ca3897b2..5cabf85f 100644
--- a/docs/development/release_guide.rst
+++ b/docs/development/release_guide.rst
@@ -8,43 +8,58 @@ during all releases.
 
 The following is a partial list of tasks to be performed for each
 release.  This list is currently under development.  Developers should
-expand the instructions while performing each release, and may use
-`Astropy's release procedures <http://docs.astropy.org/en/stable/development/releasing.html>`
-for guidance.
+expand the instructions while performing each release, and may refer to
+`Astropy's release procedures
+<http://docs.astropy.org/en/stable/development/releasing.html>` for
+guidance.
 
 Pre-release
 -----------
 
-* Check that the Continuous Integration is passing for the correct version `(see the latest commit on master) <https://github.com/PlasmaPy/PlasmaPy/commits/master>`_
+* Check that the Continuous Integration is passing for the correct
+  version `(see the latest commit on master)
+  <https://github.com/PlasmaPy/PlasmaPy/commits/master>`_
 
 * Update ``docs/about/change_log.rst``
 
 * Update ``docs/about/release_notes.rst``
 
-* Update ``.mailmap`` and ``docs/about/credits.rst`` to include new contributors
+* Update ``.mailmap`` and ``docs/about/credits.rst`` to include new
+  contributors
 
-  * use ``git shortlog -n -s -e`` for ``.mailmap``
-  * use ``astropy-tools/author_lists.py`` for ``credits.rst``
+  * Use ``git shortlog -n -s -e`` for ``.mailmap``
+  * Use ``astropy-tools/author_lists.py`` for ``credits.rst``
 
-* Edit ``setup.cfg`` to remove ``.dev`` from ``version = 0.2.0.dev``
+* Edit ``setup.cfg`` to remove ``.dev`` from ``version = x.y.z.dev``
 
-* Make sure tests pass (``python setup.py test``) and documentation builds without issue (``python setup.py build_docs -W``)
+* Reserve a digital object identifier on Zenodo, and update citation
+  information (e.g., in ``plasmapy.__citation__`` and ``README.md``)
 
-* commit your changes up until now
+* Update code metadata in ``codemeta.json``
 
-  * double-check CI on pushing this to a branch
+  * The `Codemeta standard <https://codemeta.github.io/>`_ is
+    relatively new, so check the standard for terms that have changed
+    and new terms that may apply
 
-* tag the new version with ``git tag -s v<version> -m "Tagging v<version>"``
+* Make sure that tests pass (``python setup.py test``) and that
+  documentation builds without issue (``python setup.py build_docs -W``)
 
-  * note that ``-s`` signs the commit with a GPG key
+* Commit your changes up until now
 
-* test that RTD is building the documentation correctly on release branch (and the version is correct)
+  * Double-check CI on pushing this to a branch
 
-* perform a source distribution release
+* Tag the new version with ``git tag -s v<version> -m "Tagging v<version>"``
 
-  * get a clean copy of the repository (``git clean -fxd`` or ``git clone``)
-  * use ``python setup.py build sdist``
-  * test that the sdist installs in a clean environment::
+  * Note that ``-s`` signs the commit with a GPG key
+
+* Test that RTD is building the documentation correctly on release
+  branch (and the version is correct)
+
+* Perform a source distribution release
+
+  * Get a clean copy of the repository (``git clean -fxd`` or ``git clone``)
+  * Use ``python setup.py build sdist``
+  * Test that the sdist installs in a clean environment::
 
        $ conda create -n plasmapy_release_test_v<version> numpy
        $ conda activate plasmapy_release_test_v<version>
@@ -52,7 +67,8 @@ Pre-release
        $ python -c 'import plasmapy; plasmapy.test()'
        $ conda deactivate
 
-  * Check that the `plasmapy.__version__` number is correct (``python -c 'import plasmapy; print(plasmapy.__version__)'``)
+  * Check that the `plasmapy.__version__` number is correct
+    (``python -c 'import plasmapy; print(plasmapy.__version__)'``)
 
 Release
 -------
@@ -60,7 +76,8 @@ Release
 * Create a new branch for the release that is separate from the master
   branch
   
-* Merge (via fast-forward merge with `git merge --ff-only`) changes from `master` into `stable`
+* Merge (via fast-forward merge with `git merge --ff-only`) changes
+  from ``master`` into ``stable``
 
 * Make sure all tests pass
 
@@ -73,11 +90,13 @@ Release
 Post-release
 ------------
 
-* Update ``docs/about/change_log.rst`` (Add a new heading for the next release)
+* Update ``docs/about/change_log.rst`` (Add a new heading for the next
+  release)
 
 * Update ``setup.cfg`` (increment version and add ``dev`` suffix)
 
-* Mint a release on Zenodo and get a digital object identifier (DOI)
+* Upload the release to the Zenodo record corresponding to the reserved
+  DOI
 
 * Notify plasma physics communities about the release
 
@@ -85,3 +104,4 @@ Post-release
 
 * Send release announcement to mailing list
 
+* Update the release guide to reflect any changes
diff --git a/plasmapy/physics/parameters.py b/plasmapy/physics/parameters.py
index 5a0c088a..5df450ed 100644
--- a/plasmapy/physics/parameters.py
+++ b/plasmapy/physics/parameters.py
@@ -671,6 +671,7 @@ def Hall_parameter(n,
     return gyro_frequency / coll_rate
 
 
+@utils.angular_freq_to_hz
 @utils.check_quantity(B={'units': u.T})
 def gyrofrequency(B: u.T, particle='e-', signed=False, Z=None):
     r"""Calculate the particle gyrofrequency in units of radians per second.
@@ -743,6 +744,8 @@ def gyrofrequency(B: u.T, particle='e-', signed=False, Z=None):
     >>> from astropy import units as u
     >>> gyrofrequency(0.1*u.T)
     <Quantity 1.75882002e+10 rad / s>
+    >>> gyrofrequency(0.1*u.T, to_hz=True)
+    <Quantity 2799249007.6528206 Hz>
     >>> gyrofrequency(0.1*u.T, signed=True)
     <Quantity -1.75882002e+10 rad / s>
     >>> gyrofrequency(0.01*u.T, 'p')
@@ -751,6 +754,8 @@ def gyrofrequency(B: u.T, particle='e-', signed=False, Z=None):
     <Quantity 957883.32241481 rad / s>
     >>> gyrofrequency(0.01*u.T, particle='T+')
     <Quantity 319964.54975911 rad / s>
+    >>> gyrofrequency(0.01*u.T, particle='T+', to_hz=True)
+    <Quantity 50923.93970833496 Hz>
     >>> omega_ce = gyrofrequency(0.1*u.T)
     >>> print(omega_ce)
     17588200236.02124 rad / s
@@ -916,6 +921,7 @@ def gyroradius(B: u.T,
     return r_Li.to(u.m, equivalencies=u.dimensionless_angles())
 
 
+@utils.angular_freq_to_hz
 @utils.check_quantity(
     n={'units': u.m ** -3, 'can_be_negative': False}
     )
@@ -982,10 +988,14 @@ def plasma_frequency(n: u.m**-3, particle='e-', z_mean=None):
     >>> from astropy import units as u
     >>> plasma_frequency(1e19*u.m**-3, particle='p')
     <Quantity 4.16329453e+09 rad / s>
+    >>> plasma_frequency(1e19*u.m**-3, particle='p', to_hz=True)
+    <Quantity 662608904.1071696 Hz>
     >>> plasma_frequency(1e19*u.m**-3, particle='D+')
     <Quantity 2.94462452e+09 rad / s>
     >>> plasma_frequency(1e19*u.m**-3)
     <Quantity 1.78398636e+11 rad / s>
+    >>> plasma_frequency(1e19*u.m**-3, to_hz=True)
+    <Quantity 28393024835.274036 Hz>
 
     """
 
@@ -1339,6 +1349,7 @@ def magnetic_energy_density(B: u.T):
     return E_B
 
 
+@utils.angular_freq_to_hz
 @utils.check_quantity(
     B={'units': u.T},
     n_e={'units': u.m ** -3, 'can_be_negative': False}
@@ -1392,6 +1403,8 @@ def upper_hybrid_frequency(B: u.T, n_e: u.m**-3):
     >>> from astropy import units as u
     >>> upper_hybrid_frequency(0.2*u.T, n_e=5e19*u.m**-3)
     <Quantity 4.00459419e+11 rad / s>
+    >>> upper_hybrid_frequency(0.2*u.T, n_e=5e19*u.m**-3, to_hz = True)
+    <Quantity 63735096112.815445 Hz>
 
     """
 
@@ -1402,6 +1415,7 @@ def upper_hybrid_frequency(B: u.T, n_e: u.m**-3):
     return omega_uh.to(u.rad / u.s)
 
 
+@utils.angular_freq_to_hz
 @utils.check_quantity(
     B={'units': u.T},
     n_i={'units': u.m ** -3, 'can_be_negative': False}
@@ -1465,6 +1479,8 @@ def lower_hybrid_frequency(B, n_i, ion='p+'):
     >>> from astropy import units as u
     >>> lower_hybrid_frequency(0.2*u.T, n_i=5e19*u.m**-3, ion='D+')
     <Quantity 5.78372733e+08 rad / s>
+    >>> lower_hybrid_frequency(0.2*u.T, n_i=5e19*u.m**-3, ion='D+', to_hz = True)
+    <Quantity 92050879.32941628 Hz>
 
     """
 
diff --git a/plasmapy/utils/__init__.py b/plasmapy/utils/__init__.py
index cbdd0983..76a34cdd 100644
--- a/plasmapy/utils/__init__.py
+++ b/plasmapy/utils/__init__.py
@@ -12,6 +12,7 @@
     _check_relativistic,
 )
 
+from plasmapy.utils.decorators.converter import angular_freq_to_hz
 from .decorators import preserve_signature
 
 from .exceptions import (
diff --git a/plasmapy/utils/decorators/__init__.py b/plasmapy/utils/decorators/__init__.py
index bbdc78c1..48eaf5a8 100644
--- a/plasmapy/utils/decorators/__init__.py
+++ b/plasmapy/utils/decorators/__init__.py
@@ -1,7 +1,8 @@
 """
 A module to contain various decorators used to build readable and useful code.
 """
-__all__ = ['check_relativistic', 'check_quantity', 'preserve_signature']
+__all__ = ['check_relativistic', 'check_quantity', 'preserve_signature', 'angular_freq_to_hz']
 
 from .helpers import preserve_signature
 from .checks import (check_relativistic, check_quantity)
+from .converter import angular_freq_to_hz
diff --git a/plasmapy/utils/decorators/converter.py b/plasmapy/utils/decorators/converter.py
new file mode 100644
index 00000000..61338f16
--- /dev/null
+++ b/plasmapy/utils/decorators/converter.py
@@ -0,0 +1,45 @@
+"""
+Decorator to convert units of functions in /physics methods
+"""
+__all__ = ["angular_freq_to_hz"]
+
+import functools
+import inspect
+from astropy import units as u
+from plasmapy.utils.decorators import preserve_signature
+
+
+def angular_freq_to_hz(fn):
+    # raise exception if fn uses the 'to_hz' kwarg
+    sig = inspect.signature(fn)
+    if 'to_hz' in sig.parameters:
+        raise ValueError(f"Wrapped function '{fn.__name__}' can not use keyword 'to_hz'."+
+                        " Keyword reserved for decorator functionality.")
+
+    # make new signature for fn
+    new_params = sig.parameters.copy()
+    new_params['to_hz'] = inspect.Parameter('to_hz', inspect.Parameter.POSITIONAL_OR_KEYWORD,
+                                            default=False)
+    new_sig = inspect.Signature(parameters=new_params.values(),
+                                return_annotation=sig.return_annotation)
+    fn.__signature__ = new_sig
+
+    @preserve_signature
+    @functools.wraps(fn)
+    def wrapper(*args, to_hz=False, **kwargs):
+        _result = fn(*args, **kwargs)
+        if to_hz:
+            return _result.to(u.Hz, equivalencies=[(u.cy/u.s, u.Hz)])
+        return _result
+    added_doc_bit = """
+    Other Parameters
+    ----------------
+    to_hz: bool
+        Set `True` to to convert function output from angular frequency to Hz
+    """
+    if wrapper.__doc__ is not None:
+        wrapper.__doc__ += added_doc_bit
+    else:
+        wrapper.__doc__ = added_doc_bit
+
+    return wrapper


Test patch:
diff --git a/plasmapy/physics/tests/test_parameters.py b/plasmapy/physics/tests/test_parameters.py
index 55c5fe78..094e3dea 100644
--- a/plasmapy/physics/tests/test_parameters.py
+++ b/plasmapy/physics/tests/test_parameters.py
@@ -442,17 +442,23 @@ def test_gyrofrequency():
 
     assert gyrofrequency(B).unit.is_equivalent(u.rad / u.s)
 
+    assert gyrofrequency(B, to_hz=True).unit.is_equivalent(u.Hz)
+
     assert np.isclose(gyrofrequency(1 * u.T).value, 175882008784.72018)
 
     assert np.isclose(gyrofrequency(2.4 * u.T).value,
                       422116821083.3284)
 
+    assert np.isclose(gyrofrequency(1 * u.T, to_hz=True).value,
+                      27992490076.528206)
+
     assert np.isclose(gyrofrequency(2.4 * u.T, signed=True).value,
                       -422116821083.3284)
 
     assert np.isclose(gyrofrequency(1 * u.G).cgs.value,
                       1.76e7, rtol=1e-3)
 
+
     with pytest.raises(TypeError):
         gyrofrequency(u.m)
 
@@ -663,10 +669,15 @@ def test_plasma_frequency():
 
     assert plasma_frequency(n_e).unit.is_equivalent(u.rad / u.s)
 
+    assert plasma_frequency(n_e, to_hz=True).unit.is_equivalent(u.Hz)
+
     assert np.isclose(plasma_frequency(1 * u.cm ** -3).value, 5.64e4, rtol=1e-2)
 
     assert np.isclose(plasma_frequency(1 * u.cm ** -3, particle='N').value, 3.53e2, rtol=1e-1)
 
+    assert np.isclose(plasma_frequency(1 * u.cm ** -3, particle='N', to_hz=True).value,
+                      56.19000195094519)
+
     with pytest.raises(TypeError):
         plasma_frequency(u.m ** -3)
 
@@ -904,15 +915,19 @@ def test_upper_hybrid_frequency():
     r"""Test the upper_hybrid_frequency function in parameters.py."""
 
     omega_uh = upper_hybrid_frequency(B, n_e=n_e)
+    omega_uh_hz = upper_hybrid_frequency(B, n_e=n_e, to_hz=True)
     omega_ce = gyrofrequency(B)
     omega_pe = plasma_frequency(n=n_e)
     assert omega_ce.unit.is_equivalent(u.rad / u.s)
     assert omega_pe.unit.is_equivalent(u.rad / u.s)
     assert omega_uh.unit.is_equivalent(u.rad / u.s)
+    assert omega_uh_hz.unit.is_equivalent(u.Hz)
     left_hand_side = omega_uh ** 2
     right_hand_side = omega_ce ** 2 + omega_pe ** 2
     assert np.isclose(left_hand_side.value, right_hand_side.value)
 
+    assert np.isclose(omega_uh_hz.value, 69385868857.90918)
+
     with pytest.raises(ValueError):
         upper_hybrid_frequency(5 * u.T, n_e=-1 * u.m ** -3)
 
@@ -935,6 +950,7 @@ def test_lower_hybrid_frequency():
     omega_pi = plasma_frequency(n=n_i, particle=ion)
     omega_ce = gyrofrequency(B)
     omega_lh = lower_hybrid_frequency(B, n_i=n_i, ion=ion)
+    omega_lh_hz = lower_hybrid_frequency(B, n_i=n_i, ion=ion, to_hz=True)
     assert omega_ci.unit.is_equivalent(u.rad / u.s)
     assert omega_pi.unit.is_equivalent(u.rad / u.s)
     assert omega_ce.unit.is_equivalent(u.rad / u.s)
@@ -943,6 +959,8 @@ def test_lower_hybrid_frequency():
     right_hand_side = 1 / (omega_ci ** 2 + omega_pi ** 2) + omega_ci ** -1 * omega_ce ** -1
     assert np.isclose(left_hand_side.value, right_hand_side.value)
 
+    assert np.isclose(omega_lh_hz.value, 299878691.3223296)
+
     with pytest.raises(ValueError):
         lower_hybrid_frequency(0.2 * u.T, n_i=5e19 * u.m ** -3, ion='asdfasd')
 
diff --git a/plasmapy/utils/decorators/tests/test_converters.py b/plasmapy/utils/decorators/tests/test_converters.py
new file mode 100644
index 00000000..b026d3a9
--- /dev/null
+++ b/plasmapy/utils/decorators/tests/test_converters.py
@@ -0,0 +1,15 @@
+import astropy.units as u
+import numpy as np
+
+from plasmapy.utils.decorators.converter import angular_freq_to_hz
+
+def test_conversion():
+    @angular_freq_to_hz
+    def test_function():
+        return 2 * np.pi * u.rad / u.s
+
+    assert test_function().unit == (u.rad / u.s)
+    assert test_function().value == 2 * np.pi
+    assert test_function(to_hz=True).unit == u.Hz
+    assert test_function(to_hz=True).value == 1
+